Repository: heyito/ito
Branch: main
Commit: ab9d158b61dd
Files: 400
Total size: 2.2 MB
Directory structure:
gitextract_bmhz2bpu/
├── .claude/
│ └── settings.local.json
├── .cursor/
│ └── rules/
│ ├── always.mdc
│ ├── code-conventions.mdc
│ ├── react.mdc
│ └── typescript.mdc
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── app-deploy.yml
│ ├── autolink-pr-to-issue.yml
│ ├── build-image.yml
│ ├── build.yml
│ ├── ci-controller.yml
│ ├── deploy-server.yml
│ ├── infra-deploy.yml
│ ├── native-build-check.yml
│ └── test-runner.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .vscode/
│ └── settings.json
├── AGENTS.md
├── CLAUDE.md
├── LICENSE
├── README.md
├── app/
│ ├── app.tsx
│ ├── assets/
│ │ ├── .gitignore
│ │ ├── accesssibility.webm
│ │ └── microphone.webm
│ ├── components/
│ │ ├── analytics/
│ │ │ └── index.ts
│ │ ├── auth/
│ │ │ ├── Auth0Provider.tsx
│ │ │ └── useAuth.ts
│ │ ├── home/
│ │ │ ├── HomeKit.tsx
│ │ │ ├── ProUpgradeDialog.tsx
│ │ │ └── contents/
│ │ │ ├── AboutContent.tsx
│ │ │ ├── DictionaryContent.tsx
│ │ │ ├── HomeContent.tsx
│ │ │ ├── NotesContent.tsx
│ │ │ ├── SettingsContent.tsx
│ │ │ ├── activityMessages.ts
│ │ │ └── settings/
│ │ │ ├── AccountSettingsContent.tsx
│ │ │ ├── AdvancedSettingsContent.tsx
│ │ │ ├── AudioSettingsContent.tsx
│ │ │ ├── GeneralSettingsContent.tsx
│ │ │ ├── KeyboardSettingsContent.tsx
│ │ │ └── PricingBillingSettingsContent.tsx
│ │ ├── icons/
│ │ │ ├── AppleIcon.tsx
│ │ │ ├── AppleNotesIcon.tsx
│ │ │ ├── AsterikIcon.tsx
│ │ │ ├── AudioIcon.tsx
│ │ │ ├── AvatarIcon.tsx
│ │ │ ├── ChatGPTIcon.tsx
│ │ │ ├── ClaudeIcon.tsx
│ │ │ ├── CodeWindowIcon.tsx
│ │ │ ├── ColorSchemeIcon.tsx
│ │ │ ├── CursorIcon.tsx
│ │ │ ├── DiscordIcon.tsx
│ │ │ ├── FanIcon.tsx
│ │ │ ├── GitHubIcon.tsx
│ │ │ ├── GmailIcon.tsx
│ │ │ ├── GoogleIcon.tsx
│ │ │ ├── IMessageIcon.tsx
│ │ │ ├── ItoIcon.tsx
│ │ │ ├── MicrosoftIcon.tsx
│ │ │ ├── NotionIcon.tsx
│ │ │ ├── SlackIcon.tsx
│ │ │ ├── SpeedIcon.tsx
│ │ │ ├── TotalWordsIcon.tsx
│ │ │ ├── VSCodeIcon.tsx
│ │ │ └── XIcon.tsx
│ │ ├── pill/
│ │ │ ├── Pill.tsx
│ │ │ └── contents/
│ │ │ ├── AudioBars.tsx
│ │ │ ├── AudioBarsBase.tsx
│ │ │ ├── LoadingAnimation.tsx
│ │ │ ├── PreviewAudioBars.tsx
│ │ │ └── TooltipButton.tsx
│ │ ├── ui/
│ │ │ ├── animated-checkmark.tsx
│ │ │ ├── app-orbit-image.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── button.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── keyboard-key.tsx
│ │ │ ├── keyboard-shortcut-editor.tsx
│ │ │ ├── microphone-selector.tsx
│ │ │ ├── multi-shortcut-editor.tsx
│ │ │ ├── nav-item.tsx
│ │ │ ├── note.tsx
│ │ │ ├── spinner.tsx
│ │ │ ├── status-indicator.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── tip.tsx
│ │ │ └── tooltip.tsx
│ │ ├── welcome/
│ │ │ ├── WelcomeKit.tsx
│ │ │ ├── contents/
│ │ │ │ ├── AnyAppContent.tsx
│ │ │ │ ├── CheckEmailContent.tsx
│ │ │ │ ├── CreateAccountContent.tsx
│ │ │ │ ├── DataControlContent.tsx
│ │ │ │ ├── EmailLoginContent.tsx
│ │ │ │ ├── EmailSignupContent.tsx
│ │ │ │ ├── GoodToGoContent.tsx
│ │ │ │ ├── IntroducingIntelligentModeContent.tsx
│ │ │ │ ├── KeyboardTestContext.tsx
│ │ │ │ ├── MicrophoneTestContent.tsx
│ │ │ │ ├── PermissionsContent.tsx
│ │ │ │ ├── ReferralContent.tsx
│ │ │ │ ├── SignInContent.tsx
│ │ │ │ └── TryItOutContent.tsx
│ │ │ └── styles.css
│ │ └── window/
│ │ ├── OnboardingTitlebar.tsx
│ │ ├── Titlebar.tsx
│ │ ├── TitlebarContext.tsx
│ │ └── WindowContext.tsx
│ ├── generated/
│ │ ├── buf/
│ │ │ └── validate/
│ │ │ └── validate_pb.ts
│ │ ├── ito_connect.ts
│ │ └── ito_pb.ts
│ ├── hooks/
│ │ ├── useBillingState.test.ts
│ │ ├── useBillingState.ts
│ │ ├── useDeviceChangeListener.ts
│ │ └── usePlatform.ts
│ ├── index.d.ts
│ ├── index.html
│ ├── media/
│ │ └── microphone.ts
│ ├── renderer.tsx
│ ├── sentry.ts
│ ├── store/
│ │ ├── useAdvancedSettingsStore.ts
│ │ ├── useAudioStore.ts
│ │ ├── useAuthStore.ts
│ │ ├── useDictionaryStore.ts
│ │ ├── useMainStore.ts
│ │ ├── useNotesStore.ts
│ │ ├── useOnboardingStore.ts
│ │ ├── usePermissionsStore.ts
│ │ ├── useSettingsStore.ts
│ │ ├── useShortcutEditingStore.ts
│ │ └── useUserMetadataStore.ts
│ ├── styles/
│ │ ├── app.css
│ │ ├── globals.css
│ │ └── window.css
│ └── utils/
│ ├── audioUtils.ts
│ ├── healthCheck.test.ts
│ ├── healthCheck.ts
│ ├── keyboard.test.ts
│ ├── keyboard.ts
│ └── utils.ts
├── build/
│ ├── entitlements.mac.inherit.plist
│ └── entitlements.mac.plist
├── build-app.sh
├── build-binaries.sh
├── commitlint.config.js
├── components.json
├── dev-app-update.yml
├── electron-builder.config.js
├── electron.vite.config.ts
├── eslint.config.mjs
├── lib/
│ ├── __tests__/
│ │ ├── fixtures/
│ │ │ ├── auth.ts
│ │ │ └── database.ts
│ │ ├── helpers/
│ │ │ └── testUtils.ts
│ │ ├── mocks/
│ │ │ ├── electron.ts
│ │ │ └── sqlite.ts
│ │ └── setup.ts
│ ├── auth/
│ │ ├── config.test.ts
│ │ ├── config.ts
│ │ ├── events.test.ts
│ │ └── events.ts
│ ├── clients/
│ │ ├── grpcClient.test.ts
│ │ ├── grpcClient.ts
│ │ └── itoHttpClient.ts
│ ├── constants/
│ │ ├── external-links.ts
│ │ ├── generated-defaults.ts
│ │ ├── keyboard-defaults.ts
│ │ ├── store-keys.test.ts
│ │ └── store-keys.ts
│ ├── main/
│ │ ├── app.ts
│ │ ├── appNap.ts
│ │ ├── audio/
│ │ │ ├── AudioStreamManager.test.ts
│ │ │ └── AudioStreamManager.ts
│ │ ├── autoUpdaterWrapper.ts
│ │ ├── context/
│ │ │ └── ContextGrabber.ts
│ │ ├── env.ts
│ │ ├── grammar/
│ │ │ ├── GrammarRulesService.test.ts
│ │ │ └── GrammarRulesService.ts
│ │ ├── index.d.ts
│ │ ├── interactions/
│ │ │ ├── InteractionManager.test.ts
│ │ │ └── InteractionManager.ts
│ │ ├── itoSessionManager.test.ts
│ │ ├── itoSessionManager.ts
│ │ ├── itoStreamController.test.ts
│ │ ├── itoStreamController.ts
│ │ ├── logger.ts
│ │ ├── main.ts
│ │ ├── recordingStateNotifier.ts
│ │ ├── sentry.ts
│ │ ├── sqlite/
│ │ │ ├── db.test.ts
│ │ │ ├── db.ts
│ │ │ ├── migrations.ts
│ │ │ ├── models.ts
│ │ │ ├── repo.test.ts
│ │ │ ├── repo.ts
│ │ │ ├── schema.ts
│ │ │ └── utils.ts
│ │ ├── store.test.ts
│ │ ├── store.ts
│ │ ├── syncService.test.ts
│ │ ├── syncService.ts
│ │ ├── teardown.ts
│ │ ├── text/
│ │ │ ├── TextInserter.test.ts
│ │ │ └── TextInserter.ts
│ │ ├── timing/
│ │ │ ├── TimingCollector.test.ts
│ │ │ └── TimingCollector.ts
│ │ ├── tray.ts
│ │ ├── voiceInputService.test.ts
│ │ └── voiceInputService.ts
│ ├── media/
│ │ ├── IAccessibilityContextProvider.ts
│ │ ├── active-application.test.ts
│ │ ├── active-application.ts
│ │ ├── audio.test.ts
│ │ ├── audio.ts
│ │ ├── keyboard.test.ts
│ │ ├── keyboard.ts
│ │ ├── macOSAccessibilityContextProvider.ts
│ │ ├── microphoneSetUp.ts
│ │ ├── native-interface.test.ts
│ │ ├── native-interface.ts
│ │ ├── selected-text-reader.test.ts
│ │ ├── selected-text-reader.ts
│ │ ├── systemAudio.ts
│ │ └── text-writer.ts
│ ├── preload/
│ │ ├── api.test.ts
│ │ ├── api.ts
│ │ ├── index.d.ts
│ │ └── preload.ts
│ ├── protocol/
│ │ └── index.ts
│ ├── types/
│ │ ├── cursorContext.ts
│ │ ├── ipc.ts
│ │ └── keyboard.ts
│ ├── utils/
│ │ ├── applicationDetection.ts
│ │ ├── crossPlatform.ts
│ │ ├── settings.test.ts
│ │ └── settings.ts
│ ├── utils.ts
│ └── window/
│ ├── index.ts
│ ├── ipcDev.ts
│ ├── ipcEvents.test.ts
│ └── ipcEvents.ts
├── native/
│ ├── Cargo.toml
│ ├── active-application/
│ │ ├── Cargo.toml
│ │ ├── active-application.manifest
│ │ ├── build.rs
│ │ └── src/
│ │ └── main.rs
│ ├── audio-recorder/
│ │ ├── Cargo.toml
│ │ ├── audio-recorder.manifest
│ │ ├── build.rs
│ │ └── src/
│ │ └── main.rs
│ ├── clippy.toml
│ ├── cursor-context/
│ │ ├── Package.resolved
│ │ ├── Package.swift
│ │ └── Sources/
│ │ └── cursor-context/
│ │ ├── CLI.swift
│ │ └── main.swift
│ ├── global-key-listener/
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── build.rs
│ │ ├── global-key-listener.manifest
│ │ └── src/
│ │ ├── key_codes.rs
│ │ └── main.rs
│ ├── macos-text/
│ │ ├── .gitignore
│ │ ├── Package.swift
│ │ └── Sources/
│ │ └── focused-text-reader/
│ │ └── main.swift
│ ├── rustfmt.toml
│ ├── selected-text-reader/
│ │ ├── Cargo.toml
│ │ ├── build.rs
│ │ ├── selected-text-reader.manifest
│ │ └── src/
│ │ ├── macos.rs
│ │ ├── main.rs
│ │ └── windows.rs
│ └── text-writer/
│ ├── Cargo.toml
│ ├── build.rs
│ ├── src/
│ │ ├── macos_writer.rs
│ │ ├── main.rs
│ │ └── windows_writer.rs
│ └── text-writer.manifest
├── package.json
├── resources/
│ └── build/
│ ├── entitlements.mac.plist
│ └── icon.icns
├── scripts/
│ ├── clean-app-data.js
│ └── generate-constants.js
├── server/
│ ├── .dockerignore
│ ├── Dockerfile
│ ├── README.md
│ ├── buf.gen.yaml
│ ├── buf.yaml
│ ├── docker-compose.yml
│ ├── infra/
│ │ ├── .gitignore
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── bin/
│ │ │ └── infra.ts
│ │ ├── cdk.context.json
│ │ ├── cdk.json
│ │ ├── jest.config.js
│ │ ├── lambdas/
│ │ │ ├── firehose-transform.ts
│ │ │ ├── opensearch-bootstrap.ts
│ │ │ ├── run-migration.ts
│ │ │ └── timing-merger.ts
│ │ ├── lib/
│ │ │ ├── cicd-stack.ts
│ │ │ ├── constants.ts
│ │ │ ├── helpers.ts
│ │ │ ├── network-stack.ts
│ │ │ ├── observability-stack.ts
│ │ │ ├── platform-stack.ts
│ │ │ ├── security-stack.ts
│ │ │ ├── service/
│ │ │ │ ├── fargate-task.ts
│ │ │ │ ├── firehose-config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── log-groups.ts
│ │ │ │ ├── migration-lambda.ts
│ │ │ │ └── opensearch-bootstrap.ts
│ │ │ ├── service-stack.ts
│ │ │ └── timing-config.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── package.json
│ ├── scripts/
│ │ ├── migrate-audio-to-s3.ts
│ │ ├── migrate.sh
│ │ └── setup-minio.sh
│ ├── src/
│ │ ├── auth/
│ │ │ ├── auth0Helpers.ts
│ │ │ └── userContext.ts
│ │ ├── clients/
│ │ │ ├── asrConfig.ts
│ │ │ ├── cerebrasClient.ts
│ │ │ ├── errors.ts
│ │ │ ├── groqClient.test.ts
│ │ │ ├── groqClient.ts
│ │ │ ├── intentTranscriptionConfig.ts
│ │ │ ├── llmProvider.ts
│ │ │ ├── providerUtils.ts
│ │ │ ├── providers.ts
│ │ │ └── s3storageClient.ts
│ │ ├── constants/
│ │ │ ├── generated-defaults.ts
│ │ │ ├── markers.ts
│ │ │ └── storage.ts
│ │ ├── db/
│ │ │ ├── models.ts
│ │ │ └── repo.ts
│ │ ├── db.ts
│ │ ├── generated/
│ │ │ ├── buf/
│ │ │ │ └── validate/
│ │ │ │ └── validate_pb.ts
│ │ │ ├── ito_connect.ts
│ │ │ └── ito_pb.ts
│ │ ├── index.ts
│ │ ├── ito.proto
│ │ ├── migrations/
│ │ │ ├── 1722889955000_initial_schema.js
│ │ │ ├── 1752006262324_add-raw-audio-column.js
│ │ │ ├── 1752099660683_add-duration-ms.js
│ │ │ ├── 1753297915000_add-advanced-settings.js
│ │ │ ├── 1754938499581_update-advanced-settings.js
│ │ │ ├── 1756922843670_raw-audio-reference.js
│ │ │ ├── 1760496947939_add-temporary-analytics-token.js
│ │ │ ├── 1761765111646_add-user-trials.js
│ │ │ ├── 1761778190395_add-user-subscriptions.js
│ │ │ ├── 1762468699097_add-subscription-end-at.js
│ │ │ ├── 1763753112000_make-llm-settings-nullable.js
│ │ │ └── schema/
│ │ │ └── initial.js
│ │ ├── prompts/
│ │ │ ├── transcription.test.ts
│ │ │ └── transcription.ts
│ │ ├── server.ts
│ │ ├── services/
│ │ │ ├── __tests__/
│ │ │ │ └── helpers.ts
│ │ │ ├── auth0.ts
│ │ │ ├── billing.test.ts
│ │ │ ├── billing.ts
│ │ │ ├── cloudWatchLogger.ts
│ │ │ ├── errorInterceptor.ts
│ │ │ ├── ito/
│ │ │ │ ├── audioUtils.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── helpers.ts
│ │ │ │ ├── itoService.ts
│ │ │ │ ├── timingService.ts
│ │ │ │ ├── transcribeStreamHandler.ts
│ │ │ │ ├── transcribeStreamV2Handler.ts
│ │ │ │ └── types.ts
│ │ │ ├── logging.test.ts
│ │ │ ├── logging.ts
│ │ │ ├── loggingInterceptor.ts
│ │ │ ├── stripeWebhook.test.ts
│ │ │ ├── stripeWebhook.ts
│ │ │ ├── timing/
│ │ │ │ └── ServerTimingCollector.ts
│ │ │ ├── trial.test.ts
│ │ │ ├── trial.ts
│ │ │ ├── validationInterceptor.test.ts
│ │ │ └── validationInterceptor.ts
│ │ ├── utils/
│ │ │ ├── abortUtils.ts
│ │ │ ├── audio.ts
│ │ │ ├── audioProcessing.ts
│ │ │ └── renderCallback.ts
│ │ └── validation/
│ │ ├── HeaderValidator.test.ts
│ │ ├── HeaderValidator.ts
│ │ ├── schemas.test.ts
│ │ └── schemas.ts
│ ├── test-client.ts
│ └── tsconfig.json
├── shared-constants.js
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.web.json
└── vite-env.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .claude/settings.local.json
================================================
{
"permissions": {
"allow": [
"WebFetch(domain:github.com)",
"WebFetch(domain:docs.microsoft.com)",
"Bash(bun build:rust:win:*)",
"Bash(bun runTest:*)",
"Bash(bun test:*)",
"Bash(cargo build:*)",
"Bash(git show:*)",
"WebFetch(domain:stackoverflow.com)",
"WebFetch(domain:www.electronjs.org)",
"WebFetch(domain:www.electron.build)",
"Bash(where:*)",
"Bash(cargo test)",
"Bash(cargo test:*)",
"Bash(bun:*)",
"Bash(cargo clippy:*)",
"Bash(cargo clean:*)",
"Bash(awk:*)",
"WebFetch(domain:connectrpc.com)",
"Bash(git log:*)"
],
"deny": [],
"ask": []
}
}
================================================
FILE: .cursor/rules/always.mdc
================================================
---
description:
globs:
alwaysApply: true
---
## Development cycle
1. Before writing any code, come up with an extremely good plan, review the plan, and then ask the user for permission to execute the plan.
2. Always rely on the user for manually testing
3. Always use bun as your preferred package manager.
4. Always rely on the user to install new packages / update new packages
5. Prefer code that is easy to read and self-documenting. Use comments sparingly.
6. Never use empty catch statements.
================================================
FILE: .cursor/rules/code-conventions.mdc
================================================
---
description:
globs: **/*.ts,**/*.tsx
alwaysApply: false
---
## Technologies used
- **React** - Build interactive UIs with React components
- **Tailwind CSS** - Utility-first CSS framework for rapid UI development
- **TypeScript** - Full type safety across all packages
- **PNPM Workspaces** - Fast, disk-efficient package management
- **ESLint & Prettier** - Code quality tools configured and ready to use
- **Auth** - We use Auth0
- **DB** - We use postgres in the server and SQLite in the app
- **Package manager** We use Bun as our preferred package manager. Please use bun whenever applicable.
# Code Conventions
When writing or modifying code in this project, please adhere to the following conventions:
1. **TypeScript Best Practices**: Follow standard, idiomatic TypeScript coding practices for structure, naming, and types, unless otherwise overridden.
2. **Minimal Comments**: Avoid adding comments unless they explain complex logic or non-obvious decisions. Well-written, self-explanatory code is preferred. Do not add comments that merely restate what the code does.
3. **Tests as Documentation**: Rely on comprehensive tests (which will be added later if not present) to document the behavior and usage of the code, rather than extensive comments within the code itself.
4. **File naming conventions**: Use kebab-case when naming directories, TypeScript, and other files.
Only make the exact changes I request—do not modify, remove, or alter any other code, styling, or page elements unless explicitly instructed. If my request conflicts with existing code, styling, or functionality, or if you anticipate any issues, pause execution and notify me for confirmation before proceeding. Always follow this rule for every modification. If in doubt, ask before making any change.
================================================
FILE: .cursor/rules/react.mdc
================================================
---
description:
globs: **/*.tsx
alwaysApply: false
---
React Naming Conventions:
- Use kebab-case for files and directories.
Components:
- DO not use 'use client' or 'use server' statements
- Favor named exports for components
- Ensure components are modular, reusable, and maintain a clear separation of concerns.
- Always split React components out so there is only ever one per file
- Keep logic as low as possible.
UI and Styling:
- Implement responsive design with Tailwind CSS; use a mobile-first approach
================================================
FILE: .cursor/rules/typescript.mdc
================================================
---
description:
globs: **/*.ts,**/*.tsx
alwaysApply: false
---
# TypeScript Best Practices
## Type System
- Prefer interfaces over types for object definitions
- Use type for unions, intersections, and mapped types
- NEVER use `any` or `as any` types or coercion
- Use strict TypeScript configuration
- Leverage TypeScript's built-in utility types
- Use generics for reusable type patterns
## Naming Conventions
- Use PascalCase for type names and interfaces
- Use camelCase for variables and functions
- Use UPPER_CASE for constants
- Use descriptive names with auxiliary verbs (e.g., isLoading, hasError)
- Prefix interfaces for React props with 'Props' (e.g., ButtonProps)
## Code Organization
- Keep type definitions close to where they're used
- Export types and interfaces from dedicated type files when shared
- Use barrel exports (index.ts) for organizing exports
- Place shared types in a `types.ts` file
- Co-locate component props with their components
## Functions
- Use explicit return types for public functions
- Use arrow functions for callbacks and methods
- Implement proper error handling with custom error types
- Use function overloads for complex type scenarios
- Prefer async/await over Promises
- Prefer function declarations over function expressions.
- Prefer functional programming over classes.
## Best Practices
- Enable strict mode in tsconfig.json
- Use readonly for immutable properties
- Leverage discriminated unions for type safety
- Use type guards for runtime type checking
- Implement proper null checking
- Avoid type assertions unless necessary
## Error Handling
- Do not proactively add error handling
- Handle Promise rejections properly
================================================
FILE: .gitattributes
================================================
# Mark generated protobuf/buf code as generated
# This makes GitHub collapse these files by default in PRs
app/generated/** linguist-generated=true
server/src/generated/** linguist-generated=true
================================================
FILE: .github/workflows/app-deploy.yml
================================================
# .github/workflows/app-deploy.yml
name: Run Migrations and Deploy App
on:
workflow_call:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
workflow_dispatch:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
permissions:
contents: read
id-token: write
jobs:
migrate-and-deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole
aws-region: us-west-2
- name: Run migrations
run: |
aws lambda invoke \
--function-name ${{ vars.AWS_STAGE }}-ItoDb-migration \
--payload '{}' \
out.json
cat out.json
if jq -e '.FunctionError' out.json > /dev/null; then
echo "❌ Lambda execution failed:"
echo "Message: $(jq -r '.errorMessage' out.json)"
exit 1
fi
if jq -e '.errorType' out.json > /dev/null; then
echo "❌ Migration failed:"
echo "Error: $(jq -r '.errorType' out.json)"
echo "Message: $(jq -r '.errorMessage' out.json)"
exit 1
fi
- name: Force new ECS deployment
run: |
aws ecs update-service \
--cluster ${{ vars.AWS_STAGE }}-ito-cluster \
--service ${{ vars.AWS_STAGE }}-ito-service \
--force-new-deployment
================================================
FILE: .github/workflows/autolink-pr-to-issue.yml
================================================
name: Auto-link PR to Issue
on:
pull_request:
types: [opened]
jobs:
autolink:
runs-on: ubuntu-latest
steps:
- name: Extract issue number from branch name
id: extract_issue
run: |
BRANCH_NAME="${{ github.head_ref }}"
echo "Branch name: $BRANCH_NAME"
# Extract issue number from branch name pattern ito-XXX
if [[ $BRANCH_NAME =~ ^ito-([0-9]+) ]]; then
ISSUE_NUMBER="${BASH_REMATCH[1]}"
echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
echo "Found issue number: $ISSUE_NUMBER"
else
echo "No issue number found in branch name"
echo "issue_number=" >> $GITHUB_OUTPUT
fi
- name: Link PR to Issue
if: steps.extract_issue.outputs.issue_number != ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ steps.extract_issue.outputs.issue_number }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Check if issue exists
ISSUE_EXISTS=$(gh api repos/heyito/ito/issues/$ISSUE_NUMBER --jq '.number' 2>/dev/null || echo "")
if [ -n "$ISSUE_EXISTS" ]; then
echo "Linking PR #$PR_NUMBER to issue #$ISSUE_NUMBER"
# Add comment to PR mentioning the issue (creates automatic link)
gh api repos/heyito/ito/issues/$PR_NUMBER/comments \
--method POST \
--field body="Resolves #$ISSUE_NUMBER"
echo "Successfully linked PR #$PR_NUMBER to issue #$ISSUE_NUMBER"
else
echo "Issue #$ISSUE_NUMBER does not exist, skipping autolink"
fi
================================================
FILE: .github/workflows/build-image.yml
================================================
name: Build and Push Docker Image
on:
workflow_call:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
workflow_dispatch:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
permissions:
contents: read
id-token: write
jobs:
build-and-push:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole
aws-region: us-west-2
- name: Login to ECR
run: |
aws ecr get-login-password \
| docker login \
--username AWS \
--password-stdin 287641434880.dkr.ecr.us-west-2.amazonaws.com
- name: Build & push multi-arch image
working-directory: server
run: |
docker buildx create --use
docker buildx build \
--platform linux/arm64,linux/amd64 \
--tag 287641434880.dkr.ecr.us-west-2.amazonaws.com/${{ vars.AWS_STAGE }}-ito-server:latest \
--push .
================================================
FILE: .github/workflows/build.yml
================================================
name: Build and Release
on:
release:
types: [published]
permissions:
contents: write
id-token: write # required for OIDC role assumption
jobs:
build-mac:
runs-on: macos-latest
environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: 'x86_64-apple-darwin,aarch64-apple-darwin'
- name: Set up environment
run: |
echo "VITE_AUTH0_DOMAIN=\"${{ secrets.VITE_AUTH0_DOMAIN }}\"" >> .env
echo "VITE_AUTH0_CLIENT_ID=\"${{ secrets.VITE_AUTH0_CLIENT_ID }}\"" >> .env
echo "VITE_AUTH0_AUDIENCE=\"${{ secrets.VITE_AUTH0_AUDIENCE }}\"" >> .env
echo "VITE_POSTHOG_API_KEY=\"${{ secrets.VITE_POSTHOG_API_KEY }}\"" >> .env
echo "VITE_POSTHOG_HOST=\"${{ secrets.VITE_POSTHOG_HOST }}\"" >> .env
echo "VITE_GRPC_BASE_URL=\"${{ vars.VITE_GRPC_BASE_URL }}\"" >> .env
echo "VITE_UPDATER_BUCKET=\"${{ vars.VITE_UPDATER_BUCKET }}\"" >> .env
echo "VITE_SENTRY_DSN=\"${{ vars.VITE_SENTRY_DSN }}\"" >> .env
echo "VITE_SENTRY_ENV=\"${{ vars.VITE_SENTRY_ENV }}\"" >> .env
echo "VITE_SENTRY_TRACES_SAMPLE_RATE=\"${{ vars.VITE_SENTRY_TRACES_SAMPLE_RATE }}\"" >> .env
echo "VITE_SENTRY_PROFILES_SAMPLE_RATE=\"${{ vars.VITE_SENTRY_PROFILES_SAMPLE_RATE }}\"" >> .env
echo "VITE_ITO_VERSION=\"${GITHUB_REF#refs/tags/v}\"" >> .env
echo "ITO_ENV=\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\"" >> .env
echo "VITE_ITO_ENV=\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\"" >> .env
echo "APPLE_ID=\"${{ secrets.APPLE_ID }}\"" >> .env
echo "APPLE_TEAM_ID=\"${{ secrets.APPLE_TEAM_ID }}\"" >> .env
echo "APPLE_APP_SPECIFIC_PASSWORD=\"${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}\"" >> .env
echo "CSC_LINK=\"release.p12\"" >> .env
echo "CSC_KEY_PASSWORD=\"${{ secrets.MACOS_CERT_PASSWORD }}\"" >> .env
echo "GH_TOKEN=\"${{ secrets.GITHUB_TOKEN }}\"" >> .env
echo "GRPC_BASE_URL=\"${{ vars.GRPC_BASE_URL }}\"" >> .env
echo "Created .env file:"
cat .env
- name: Decode and install certificate
run: |
echo "${{ secrets.MACOS_CERT_BASE64 }}" | base64 --decode > release.p12
- name: Build and package macOS application
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: ./build-app.sh mac
- name: Upload Mac Installer DMG
uses: actions/upload-artifact@v4
with:
name: Ito-Mac-Installer
path: dist/Ito-${{ github.event.release.target_commitish == 'main' && 'Installer' || 'dev-Installer' }}.dmg
- name: Upload Mac Build Artifacts
uses: actions/upload-artifact@v4
with:
name: Mac-Build-Artifacts
path: |
dist/*.yml
dist/*universal-mac.zip
dist/*universal-mac.zip.blockmap
dist/*.dmg
dist/*.dmg.blockmap
build-windows-rust:
runs-on: windows-latest
environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Rust with MSVC toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable-x86_64-pc-windows-msvc
targets: 'x86_64-pc-windows-msvc'
- name: Build Rust native modules with MSVC
shell: pwsh
run: |
# Find Visual Studio installation (GitHub Actions uses Enterprise)
$vsPath = if (Test-Path "C:\Program Files\Microsoft Visual Studio\2022\Enterprise") {
"C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
} elseif (Test-Path "C:\Program Files\Microsoft Visual Studio\2022\Community") {
"C:\Program Files\Microsoft Visual Studio\2022\Community"
} else {
throw "Visual Studio 2022 not found"
}
Write-Host "Using Visual Studio at: $vsPath"
# Launch VS Developer PowerShell and build
& "$vsPath\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64
# Build each native module
$modules = @("global-key-listener", "audio-recorder", "text-writer", "active-application", "selected-text-reader")
foreach ($module in $modules) {
Write-Host "Building $module with MSVC..."
Set-Location "native\$module"
cargo build --release --target x86_64-pc-windows-msvc
Set-Location ..\..
}
- name: Prepare artifacts directory
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path rust-binaries
$modules = @("global-key-listener", "audio-recorder", "text-writer", "active-application", "selected-text-reader")
foreach ($module in $modules) {
$sourcePath = "native\target\x86_64-pc-windows-msvc\release\$module.exe"
$destPath = "rust-binaries\$module.exe"
if (Test-Path $sourcePath) {
Copy-Item $sourcePath $destPath
Write-Host "Copied $module.exe"
} else {
Write-Error "$sourcePath not found!"
exit 1
}
}
- name: Upload Rust binaries
uses: actions/upload-artifact@v4
with:
name: Windows-Rust-Binaries
path: rust-binaries/*.exe
build-windows:
runs-on: ubuntu-latest
needs: build-windows-rust
environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download Rust binaries built with MSVC
uses: actions/download-artifact@v4
with:
name: Windows-Rust-Binaries
path: rust-binaries-msvc
- name: Place Rust binaries in expected locations
run: |
# Place binaries where electron-builder expects them
for module in global-key-listener audio-recorder text-writer active-application selected-text-reader; do
mkdir -p "native/target/x86_64-pc-windows-msvc/release"
cp "rust-binaries-msvc/$module.exe" "native/target/x86_64-pc-windows-msvc/release/"
echo "Placed $module.exe"
done
- name: Set up Bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Set up environment
run: |
echo "VITE_AUTH0_DOMAIN=\"${{ secrets.VITE_AUTH0_DOMAIN }}\"" >> .env
echo "VITE_AUTH0_CLIENT_ID=\"${{ secrets.VITE_AUTH0_CLIENT_ID }}\"" >> .env
echo "VITE_AUTH0_AUDIENCE=\"${{ secrets.VITE_AUTH0_AUDIENCE }}\"" >> .env
echo "VITE_POSTHOG_API_KEY=\"${{ secrets.VITE_POSTHOG_API_KEY }}\"" >> .env
echo "VITE_POSTHOG_HOST=\"${{ secrets.VITE_POSTHOG_HOST }}\"" >> .env
echo "VITE_GRPC_BASE_URL=\"${{ vars.VITE_GRPC_BASE_URL }}\"" >> .env
echo "VITE_UPDATER_BUCKET=\"${{ vars.VITE_UPDATER_BUCKET }}\"" >> .env
echo "VITE_SENTRY_DSN=\"${{ vars.VITE_SENTRY_DSN }}\"" >> .env
echo "VITE_SENTRY_ENV=\"${{ vars.VITE_SENTRY_ENV }}\"" >> .env
echo "VITE_SENTRY_TRACES_SAMPLE_RATE=\"${{ vars.VITE_SENTRY_TRACES_SAMPLE_RATE }}\"" >> .env
echo "VITE_SENTRY_PROFILES_SAMPLE_RATE=\"${{ vars.VITE_SENTRY_PROFILES_SAMPLE_RATE }}\"" >> .env
echo "VITE_ITO_VERSION=\"${GITHUB_REF#refs/tags/v}\"" >> .env
echo "ITO_ENV=\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\"" >> .env
echo "VITE_ITO_ENV=\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\"" >> .env
echo "GH_TOKEN=\"${{ secrets.GITHUB_TOKEN }}\"" >> .env
echo "GRPC_BASE_URL=\"${{ vars.GRPC_BASE_URL }}\"" >> .env
echo "Created .env file:"
cat .env
- name: Build and package Windows application (unsigned)
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: ./build-app.sh windows --skip-binaries
- name: Upload unsigned Windows artifacts for signing
uses: actions/upload-artifact@v4
with:
name: Windows-Unsigned-Artifacts
path: |
dist/*.exe
dist/*.yml
dist/*.nsis.7z
dist/*.zip
dist/*.blockmap
sign-windows:
runs-on: windows-latest
needs: build-windows
environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}
steps:
- name: Download unsigned Windows artifacts
uses: actions/download-artifact@v4
with:
name: Windows-Unsigned-Artifacts
path: dist
- name: Sign Windows executable with Azure Trusted Signing
uses: Azure/trusted-signing-action@v0
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME }}
files-folder: dist
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Regenerate metadata after signing
shell: bash
run: |
# Find the signed executable file in the dist directory
# This searches for any file matching the pattern "Ito-*.exe"
SIGNED_EXE=$(find dist -name "Ito-*.exe" | head -1)
if [ -z "$SIGNED_EXE" ]; then
echo "Error: No signed executable found"
exit 1
fi
echo "Found signed executable: $SIGNED_EXE"
# Calculate the SHA512 checksum of the signed file
# cut -d' ' -f1 extracts just the hash part (before the filename)
NEW_CHECKSUM=$(sha512sum "$SIGNED_EXE" | cut -d' ' -f1)
# Convert hex checksum to base64 format (required by electron-updater)
# xxd -r -p converts hex string to raw bytes
# base64 -w 0 encodes to base64 without line wrapping
NEW_CHECKSUM_B64=$(echo -n "$NEW_CHECKSUM" | xxd -r -p | base64 -w 0)
# Get the file size in bytes using stat
FILE_SIZE=$(stat -c%s "$SIGNED_EXE")
# Extract just the filename from the full path
FILENAME=$(basename "$SIGNED_EXE")
echo "New checksum (hex): $NEW_CHECKSUM"
echo "New checksum (base64): $NEW_CHECKSUM_B64"
echo "File size: $FILE_SIZE"
echo "Filename: $FILENAME"
# Update the auto-updater metadata file with the signed file's information
# This is critical because signing changes the file's checksum
if [ -f "dist/latest.yml" ]; then
echo "Updating latest.yml with new checksum after signing"
# Update each field in latest.yml using sed (stream editor)
# The | delimiter is used instead of / to avoid conflicts with filenames
sed -i "s|url: .*|url: $FILENAME|" dist/latest.yml # Update download URL
sed -i "s|sha512: .*|sha512: $NEW_CHECKSUM_B64|" dist/latest.yml # Update checksum
sed -i "s|size: .*|size: $FILE_SIZE|" dist/latest.yml # Update file size
sed -i "s|path: .*|path: $FILENAME|" dist/latest.yml # Update path
echo "Updated latest.yml contents:"
cat dist/latest.yml
else
echo "Warning: latest.yml not found in dist directory"
ls -la dist/
fi
- name: Upload signed Windows artifacts
uses: actions/upload-artifact@v4
with:
name: Windows-Build-Artifacts
path: |
dist/*.yml
dist/*.exe
dist/*.nsis.7z
dist/*.zip
dist/*.blockmap
upload-to-s3:
runs-on: ubuntu-latest
needs: [build-mac, sign-windows]
environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}
steps:
- name: Download Mac Build Artifacts
uses: actions/download-artifact@v4
with:
name: Mac-Build-Artifacts
path: mac-dist
- name: Download Windows Build Artifacts
uses: actions/download-artifact@v4
with:
name: Windows-Build-Artifacts
path: windows-dist
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole
aws-region: us-west-2
- name: Upload Build Output to S3
run: |
echo "Deploying version ${{ github.ref_name }} to S3"
BUCKET=s3://${{ vars.AWS_STAGE }}-ito-releases/releases
echo "Listing existing files in root of releases/"
aws s3 ls "$BUCKET/" | grep -vE '/$' | awk '{print $4}' > existing_files.txt
echo "Combining Mac and Windows artifacts"
mkdir -p combined-dist
# Copy Mac artifacts
if [ -d "mac-dist" ]; then
cp -r mac-dist/* combined-dist/
fi
# Copy Windows artifacts
if [ -d "windows-dist" ]; then
cp -r windows-dist/* combined-dist/
fi
echo "Files to upload:"
ls -la combined-dist/
echo "Identifying which files to delete post-upload"
find combined-dist -maxdepth 1 \( \
-name '*.yml' -o \
-name '*universal-mac.zip' -o \
-name '*universal-mac.zip.blockmap' -o \
-name '*.dmg' -o \
-name '*.dmg.blockmap' -o \
-name '*.exe' -o \
-name '*.nsis.7z' -o \
-name '*.zip' -o \
-name '*.blockmap' \
\) | xargs -I{} basename {} > uploaded_root_files.txt
echo "Uploading full combined dist to versioned folder: $BUCKET/${{ github.ref_name }}/"
aws s3 cp combined-dist $BUCKET/${{ github.ref_name }}/ --recursive
echo "Uploading selected files to root of releases/"
for FILE in \
$(find combined-dist -maxdepth 1 -name '*.yml') \
$(find combined-dist -maxdepth 1 -name '*universal-mac.zip') \
$(find combined-dist -maxdepth 1 -name '*universal-mac.zip.blockmap') \
$(find combined-dist -maxdepth 1 -name '*.dmg') \
$(find combined-dist -maxdepth 1 -name '*.dmg.blockmap') \
$(find combined-dist -maxdepth 1 -name '*.exe') \
$(find combined-dist -maxdepth 1 -name '*.nsis.7z') \
$(find combined-dist -maxdepth 1 -name '*.zip') \
$(find combined-dist -maxdepth 1 -name '*.blockmap')
do
if [ -f "$FILE" ]; then
aws s3 cp "$FILE" $BUCKET/
fi
done
# Compare and find stale files (exist in bucket but not in upload list)
# Exclude .blockmap files from deletion to preserve differential update capability
echo "Existing files in bucket root:"
cat existing_files.txt
echo ""
echo "Files uploaded to bucket root:"
cat uploaded_root_files.txt
echo ""
echo "Computing stale files (existing - uploaded)..."
comm -23 <(sort existing_files.txt) <(sort uploaded_root_files.txt) > all_stale_files.txt
echo "All stale files before filtering:"
cat all_stale_files.txt || echo "(none)"
echo ""
echo "Filtering out .blockmap files..."
grep -v '\.blockmap$' all_stale_files.txt > stale_files.txt || echo "(no non-blockmap stale files found)"
echo "Stale files identified for deletion (excluding .blockmap files for differential updates):"
cat stale_files.txt || echo "(none)"
# Delete each stale file (but preserve blockmaps)
while IFS= read -r file; do
echo "Deleting stale file: $file"
aws s3 rm "$BUCKET/$file"
done < stale_files.txt
- name: Invalidate CDN Cache for Release Files
run: |
echo "Invalidating cache for release files on distribution: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}. Updated files should be available within 15 minutes."
aws cloudfront create-invalidation \
--distribution-id "${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}" \
--paths "/*.yml" "/*.dmg" "/*.exe" "/*.zip" "/*.blockmap"
- name: Upload installers to GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.ref_name }}
files: |
mac-dist/*.dmg
windows-dist/*.exe
================================================
FILE: .github/workflows/ci-controller.yml
================================================
name: CI Controller
on:
push:
branches:
- 'main'
- 'dev'
pull_request:
branches:
- '**'
jobs:
run-tests:
uses: ./.github/workflows/test-runner.yml
native-build-check:
uses: ./.github/workflows/native-build-check.yml
# This job ONLY runs for pushes to 'dev' and 'main'
deploy-server:
needs: run-tests
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'dev')
uses: ./.github/workflows/deploy-server.yml
with:
# Dynamically set the environment based on the branch
environment: ${{ github.ref_name == 'main' && 'production' || 'develop' }}
secrets: inherit
================================================
FILE: .github/workflows/deploy-server.yml
================================================
name: Deploy Server Worfklow
on:
workflow_call:
inputs:
environment:
type: string
required: true
jobs:
paths-check:
runs-on: ubuntu-latest
outputs:
server_changed: ${{ steps.filter.outputs.server }}
infra_changed: ${{ steps.filter.outputs.infra }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch full history to allow comparing against previous commits
- name: Detect changed areas
id: filter
uses: dorny/paths-filter@v2
with:
base: ${{ github.event.before }}
filters: |
server:
- server/src/**
- server/Dockerfile
- server/package.json
infra:
- server/infra/**
list-files: 'none'
# Step 1: Build and push Docker image first (if server code changed)
# This ensures :latest tag is updated before CDK references it
build-image:
needs: paths-check
if: ${{ needs.paths-check.outputs.server_changed == 'true' }}
uses: ./.github/workflows/build-image.yml
with:
environment: ${{ inputs.environment }}
secrets: inherit
# Step 2: Deploy infrastructure (always runs if anything changed)
# If task definition changes, ECS will automatically restart with new :latest
deploy-infra:
needs:
- paths-check
- build-image
if: ${{ always() && (needs.paths-check.outputs.infra_changed == 'true' || needs.paths-check.outputs.server_changed == 'true') && (needs.build-image.result == 'success' || needs.build-image.result == 'skipped') }}
uses: ./.github/workflows/infra-deploy.yml
with:
environment: ${{ inputs.environment }}
secrets: inherit
# Step 3: Run migrations and force ECS deployment (only if server changed)
# Migration + force-new-deployment ensures ECS picks up new image even if task def didn't change
deploy-app:
needs:
- paths-check
- build-image
- deploy-infra
if: ${{ needs.paths-check.outputs.server_changed == 'true' }}
uses: ./.github/workflows/app-deploy.yml
with:
environment: ${{ inputs.environment }}
secrets: inherit
================================================
FILE: .github/workflows/infra-deploy.yml
================================================
name: Infra CDK
on:
workflow_call:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
workflow_dispatch:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
permissions:
contents: read
id-token: write # for OIDC
jobs:
cdk-deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Checkout infra
uses: actions/checkout@v3
- name: Setup Node.js & CDK
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install infra dependencies
working-directory: server/infra
run: npm install
- name: Verify CDK version
working-directory: server/infra
run: npx cdk --version
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole
aws-region: us-west-2
- name: Synthesize CDK template
working-directory: server/infra
run: npx cdk synth
- name: Show CDK diff
working-directory: server/infra
run: npx cdk diff
- name: Refresh AWS credentials before deploy
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole
aws-region: us-west-2
- name: Deploy to AWS
working-directory: server/infra
env:
AUTH0_DOMAIN: '${{ secrets.VITE_AUTH0_DOMAIN }}'
AUTH0_AUDIENCE: '${{ secrets.VITE_AUTH0_AUDIENCE }}'
AUTH0_CLIENT_ID: '${{ secrets.VITE_AUTH0_CLIENT_ID }}'
AUTH0_MGMT_CLIENT_ID: '${{ secrets.AUTH0_MGMT_CLIENT_ID }}'
AUTH0_MGMT_CLIENT_SECRET: '${{ secrets.AUTH0_MGMT_CLIENT_SECRET }}'
STRIPE_SECRET_KEY: '${{ secrets.STRIPE_SECRET_KEY }}'
STRIPE_WEBHOOK_SECRET: '${{ secrets.STRIPE_WEBHOOK_SECRET }}'
STRIPE_PRICE_ID: '${{ secrets.STRIPE_PRICE_ID }}'
APP_PROTOCOL: '${{ vars.APP_PROTOCOL }}'
STRIPE_PUBLIC_BASE_URL: '${{ vars.STRIPE_PUBLIC_BASE_URL }}'
run: npx cdk deploy "Ito-${{ vars.AWS_STAGE }}/*" --require-approval never
================================================
FILE: .github/workflows/native-build-check.yml
================================================
name: Native Build Check
on:
workflow_call:
jobs:
build-check-mac:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: 'x86_64-apple-darwin,aarch64-apple-darwin'
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
native/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Verify native binaries compile (x86_64)
working-directory: native
run: cargo build --workspace --release --target x86_64-apple-darwin
- name: Verify native binaries compile (aarch64)
working-directory: native
run: cargo build --workspace --release --target aarch64-apple-darwin
build-check-windows:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y mingw-w64
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: 'x86_64-pc-windows-gnu'
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
native/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Verify native binaries compile (Windows)
working-directory: native
run: cargo build --workspace --release --target x86_64-pc-windows-gnu
================================================
FILE: .github/workflows/test-runner.yml
================================================
name: Test Runner
on:
workflow_call:
jobs:
test:
runs-on: macos-latest
env:
ITO_ENV: dev
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v1
- name: Set ITO_ENV based on branch
run: |
if [[ "${GITHUB_REF_NAME}" == "main" || "${GITHUB_BASE_REF}" == "main" ]]; then
echo "ITO_ENV=prod" >> "$GITHUB_ENV"
else
echo "ITO_ENV=dev" >> "$GITHUB_ENV"
fi
- name: Install dependencies
run: bun install && cd server && bun install && cd ..
- name: Run tests
run: bun runAllTests
- name: Check code formatting
run: bun format:app
- name: Check code linting
run: bun lint:app
- name: Check Rust formatting
run: bun format:native
- name: Check Rust linting
run: bun lint:native
================================================
FILE: .gitignore
================================================
out/*
release/*
node_modules
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
*/dist/*
dist/*
.DS_Store
native/**/target
native/target-rust-analyzer
native/**/.build
out.json
tsconfig.node.tsbuildinfo
server/deploy-dev.sh
# Sentry Config File
.env.sentry-build-plugin
.electron-builder-cache
.npm-cache
.cache
================================================
FILE: .prettierignore
================================================
node_modules/
dist/
build/
.next/
coverage/
out/
# Generated proto files
**/generated/
lib/generated/
app/generated/
server/src/generated/
# CDK outputs
server/infra/cdk.out/
server/infra/*.d.ts
server/infra/*.js
*.assets.json
*.template.json
# Build artifacts
*.min.js
*.bundle.js
# Dependencies
.pnpm-store/
bun.lockb
pnpm-lock.yaml
yarn.lock
package-lock.json
# Test coverage
.nyc_output/
# IDE and system files
.vscode/
.idea/
.DS_Store
*.swp
*.swo
# Logs
*.log
logs/
# Environment files
.env
.env.*
# Electron
.vite/
# Binary files
resources/binaries/
*.exe
*.dll
*.so
*.dylib
# Native build artifacts
native/**/.build/**
native/**/build/**
native/**/*.json
================================================
FILE: .prettierrc.json
================================================
{
"semi": false,
"singleQuote": true,
"arrowParens": "avoid",
"tabWidth": 2
}
================================================
FILE: .vscode/settings.json
================================================
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"rust-analyzer.linkedProjects": [
"./native/selected-text-reader/Cargo.toml",
"./native/text-writer/Cargo.toml",
"./native/audio-recorder/Cargo.toml",
"./native/active-application/Cargo.toml",
"./native/global-key-listener/Cargo.toml"
],
"rust-analyzer.cargo.targetDir": "${workspaceFolder}/native/target-rust-analyzer"
}
================================================
FILE: AGENTS.md
================================================
# Repository Guidelines
## Project Structure & Module Organization
- `app/` hosts the Electron renderer (React + Tailwind); `lib/` contains shared TypeScript modules, preload logic, and unit tests.
- `server/` holds the Bun-based transcription API, database migrations, and infrastructure scripts.
- `native/` includes Rust crates for keyboard, audio, and text bridges; rebuild via scripts as needed.
- `resources/` provides packaging assets (icons, updater configs), while `build/` and `out/` house generated installers; avoid editing build outputs.
- Use `scripts/` for automation and keep generated proto/constants under `lib/generated` (created by build steps).
## Build, Test, and Development Commands
- `bun install` aligns dependencies after pulls.
- `bun run dev` starts the Electron shell with live reload; `bun run dev:rust` rebuilds native bridges via `./build-binaries.sh` before launching.
- Packaging: `bun run build:app` for cross-platform; use `bun run build:mac` / `bun run build:win` for platform installers.
- Backend (`server/`): `bun install`, `bun run local-db-up` (Postgres), `bun run db:migrate`, `bun run dev`.
## Coding Style & Naming Conventions
- TypeScript + React across app/lib; server targets modern ECMAScript on Bun.
- Prettier (2-space indent) and ESLint enforce formatting. Run `bun run format` and `bun run lint` (or `*:app` variants) before submitting.
- Components/classes use `PascalCase`, hooks/utilities `camelCase`, constants `SCREAMING_SNAKE_CASE`. Co-locate Tailwind styles with components and reuse tokens via `lib/constants`.
- Always prefer console commands over log commands. E.g. use `console.log` instead of `log.info`.
## Testing Guidelines
- Unit tests run with Bun. Renderer/shared specs live in `lib/__tests__`; server tests reside under `server/src/**`. Name files with `.test.ts`.
- Run `bun run runLibTests`, `bun run runServerTests`, or `bun run runAllTests`; paste output in PR notes.
- Seed backend tests with `bun run local-db-up` and avoid hitting external services—mock microphone, OS, and network dependencies.
## Commit & Pull Request Guidelines
- Conventional commits are enforced via commitlint (example: `feat(app): add dictation overlay`). Scope by top-level folder.
- PRs need a summary, linked issue, test commands, and screenshots/GIFs for UI work. Call out schema/config updates and refresh `.env.example` files.
## Environment & Security Notes
- Copy `.env.example` (root) and `server/.env.example`; never commit credentials.
- After modifying protobufs or constants, run `bun run generate:constants` so `lib/generated` stays in sync with the app bundle.
================================================
FILE: CLAUDE.md
================================================
# Claude Context for ITO Project
## Project Overview
This is the ITO project - an AI assistant application with both client and server components.
## Project Structure
- `app/` - Client application code
- `server/` - Server-side code with gRPC services
- `server/src/ito.proto` - Protocol buffer definitions
- `server/src/clients/` - Various client implementations (Groq, LLM providers, etc.)
## Branch
Main development branch: `dev`
## Development Commands
- Dev: `bun dev` (starts electron-vite dev with watch)
- Server: `docker compose up --build` (run from server directory)
- Build: `bun build:app:mac` or `bun build:app:windows`
- Test: `bun runAllTests` (runs lib, server, and native tests)
- Lib tests: `bun runLibTests`
- Server tests: `bun runServerTests`
- Native tests: `bun runNativeTests` (or see "Native Binary Tests" section)
- Lint:
- TypeScript: `bun lint` (check) or `bun lint:fix` (fix)
- Rust: `bun lint:native` (check) or `bun lint:fix:native` (fix)
- Type check: `bun type-check`
- Format:
- TypeScript: `bun format` (check) or `bun format:fix` (fix)
- Rust: `bun format:native` (check) or `bun format:fix:native` (fix)
## Native Binary Tests
The `native/` directory contains Rust binaries that power the app's core functionality. The modules are organized as a Cargo workspace, allowing you to test and build all modules with a single command.
### Running Tests
Test all native modules:
```bash
cd native
cargo test --workspace
```
Or use the npm script:
```bash
bun runNativeTests
```
Test a single module:
```bash
cd native/global-key-listener
cargo test
```
### Native Modules
- `global-key-listener` - Keyboard event capture and hotkey management
- `audio-recorder` - Audio recording with sample rate conversion
- `text-writer` - Cross-platform text input simulation
- `active-application` - Active window detection
- `selected-text-reader` - Selected text extraction
### Linting and Formatting
Rust code follows standard formatting and linting rules defined in `native/`:
- **rustfmt.toml** - Code formatting configuration (100 char width, Unix line endings)
- **clippy.toml** - Linter configuration (cognitive complexity threshold)
- **Cargo.toml** - Workspace-level lint rules (pedantic + nursery warnings)
Run checks locally:
```bash
# Check formatting
bun format:native
# Auto-fix formatting
bun format:fix:native
# Check lints
bun lint:native
# Auto-fix lints (where possible)
bun lint:fix:native
```
### CI/CD
Native tests and builds are integrated into the existing CI workflows:
**Tests** (`.github/workflows/test-runner.yml`):
- Unit tests run on macOS runner (OS-agnostic tests)
- Runs automatically via `bun runAllTests` on all pushes and PRs
- Executed as part of the main CI controller workflow
**Compilation Checks** (`.github/workflows/native-build-check.yml`):
- macOS: Verifies compilation for x86_64 and aarch64 architectures
- Windows: Verifies cross-compilation for x86_64-pc-windows-gnu
- Runs automatically on all pushes and PRs via the CI controller
- Ensures binaries compile correctly for both platforms before merging
**Release Builds** (`.github/workflows/build.yml`):
- Full release compilation happens during tagged releases
- Also includes compilation verification before packaging
## Code Style Preferences
- Keep code as simple as possible
- Don't create overly long files
- Group related code into useful, well-named functions
- Prefer clean, readable code over complex solutions
- Follow existing patterns and conventions in the codebase
- Always prefer console commands over log commands. E.g. use `console.log` instead of `log.info`.
## Tech Stack
- TypeScript
- bun
- gRPC with Protocol Buffers
- React (for UI components)
- Various LLM providers (Groq, etc.)
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2024 Demox Labs
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
================================================
FILE: README.md
================================================
# [DEPRECATED] - This project is no longer maintained
# Ito
Smart dictation. Everywhere you want.
Ito is an intelligent voice assistant that brings seamless voice dictation to any application on your computer. Simply hold down your trigger key, speak naturally, and watch your words appear instantly in any text field.
---
## ✨ Features
### 🎙️ **Universal Voice Dictation**
- **Works in any app**: Emails, documents, chat applications, web browsers, code editors
- **Global keyboard shortcuts**: Customizable trigger keys that work system-wide
- **Real-time transcription**: High-accuracy speech-to-text powered by advanced AI models
- **Instant text insertion**: Automatically types transcribed text into the focused text field
### 🧠 **Smart & Adaptive**
- **Custom dictionary**: Add technical terms, names, and specialized vocabulary
- **Context awareness**: Learns from your usage patterns to improve accuracy
- **Multi-language support**: Transcribe in multiple languages
- **Intelligent punctuation**: Automatically adds appropriate punctuation
### ⚙️ **Powerful Customization**
- **Flexible shortcuts**: Configure any key combination as your trigger
- **Audio preferences**: Choose your preferred microphone
- **Privacy controls**: Local processing options and data control settings
- **Seamless integration**: Works with any application
### 💾 **Data Management**
- **Notes system**: Automatically save transcriptions for later reference
- **Interaction history**: Track your dictation sessions and improve over time
- **Cloud sync**: Keep your settings and data synchronized across devices
- **Export capabilities**: Export your notes and interaction data
---
## 🚀 Quick Start
### Prerequisites
- **macOS 10.15+** or **Windows 10+**
- **Node.js 20+** and **Bun** (for development)
- **Rust toolchain** (for building native components)
- **Microphone access** and **Accessibility permissions**
### Installation
1. **Download the latest release** from [heyito.ai](https://www.heyito.ai/) or the [GitHub releases page](https://github.com/heyito/ito/releases)
2. **Install the application**:
- **macOS**: Open the `.dmg` file and drag Ito to Applications
- **Windows**: Run the `.exe` installer and follow the setup wizard
3. **Grant permissions** when prompted:
- **Microphone access**: Required for voice input
- **Accessibility access**: Required for global keyboard shortcuts and text insertion
4. **Set up authentication**:
- Sign in with Google, Apple, Github through Auth0 or create a local account
- Complete the guided onboarding process
### First Use
1. **Configure your trigger key**: Choose a comfortable keyboard shortcut (default: `Fn + Space`)
2. **Test your microphone**: Ensure clear audio input during the setup process
3. **Try it out**: Hold your trigger key and speak into any text field
4. **Customize settings**: Adjust voice sensitivity, shortcuts, and preferences
---
## 🛠️ Development
### Building from Source
> **Important**: Ito requires a local transcription server for voice processing. See [server/README.md](server/README.md) for detailed server setup instructions.
```bash
# Clone the repository
git clone https://github.com/heyito/ito.git
cd ito
# Install dependencies
bun install
# Set up environment variables
cp .env.example .env
# Build native components (Rust binaries)
./build-binaries.sh
# Set up and start the server (required for transcription)
cd server
cp .env.example .env # Edit with your API keys
bun install
bun run local-db-up # Start PostgreSQL database
bun run db:migrate # Run database migrations
bun run dev # Start development server
cd ..
# Start the Electron app (in a new terminal)
bun run dev
```
### Build Requirements
#### All Platforms
- **Rust**: Install via [rustup.rs](https://rustup.rs/)
- **Windows users**: See Windows-specific instructions below for GNU toolchain setup
- **macOS/Linux users**: Default installation is sufficient
#### macOS
- **Xcode Command Line Tools**: `xcode-select --install`
#### Windows
**Required Setup:**
This setup uses git bash for shell operations. Download from [git](https://git-scm.com/downloads)
1. **Install Docker Desktop**: Download from [docker.com](https://www.docker.com/products/docker-desktop/) and ensure it's running
2. **Install Rust** (with GNU target)
Download and run the official [Rust installer for Windows](https://rustup.rs/).
This installs `rustup` and the MSVC toolchain by default.
Add the GNU target (needed for our native components):
rustup toolchain install stable-x86_64-pc-windows-gnu
rustup target add x86_64-pc-windows-gnu
---
3. **Install 7-Zip**
winget install 7zip.7zip
---
4. **Install GCC & MinGW-w64 via MSYS2**
Install [MSYS2](https://www.msys2.org/).
Open the **MSYS2 MinGW x64** shell (from the Start Menu).
Update and install the toolchain:
pacman -Syu # run twice if asked to restart
pacman -S --needed mingw-w64-x86_64-toolchain
Verify the tools exist:
ls /mingw64/bin/gcc.exe /mingw64/bin/dlltool.exe
---
5. **Use the MinGW tools when building** (Git Bash)
You normally develop and build in **Git Bash**. Before building, prepend the MinGW path:
export PATH="/c/msys64/mingw64/bin:$PATH"
export DLLTOOL="/c/msys64/mingw64/bin/dlltool.exe"
export CC_x86_64_pc_windows_gnu="/c/msys64/mingw64/bin/x86_64-w64-mingw32-gcc.exe"
export AR_x86_64_pc_windows_gnu="/c/msys64/mingw64/bin/ar.exe"
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="/c/msys64/mingw64/bin/x86_64-w64-mingw32-gcc.exe"
Check you’re picking up the right ones:
which gcc # -> /c/msys64/mingw64/bin/gcc.exe
which dlltool # -> /c/msys64/mingw64/bin/dlltool.exe
⚠️ **Do not add `C:\msys64\ucrt64\bin` to PATH.** That’s the wrong runtime and will break linking.
💡 To avoid running these exports every session, add the lines above to your Git Bash `~/.bashrc` file. They will be applied automatically whenever you open a new Git Bash window.
---
6. **Restart Git Bash if you update MSYS2**
Whenever you update MSYS2 packages with `pacman -Syu`, restart Git Bash so the changes take effect.
> **Note**: Windows builds use Docker for cross-compilation to ensure consistent builds. The Docker container handles the Windows build environment automatically.
### Project Structure
```
ito/
├── app/ # Electron renderer (React frontend)
│ ├── components/ # React components
│ ├── store/ # Zustand state management
│ └── styles/ # TailwindCSS styles
├── lib/ # Shared library code
│ ├── main/ # Electron main process
│ ├── preload/ # Preload scripts & IPC
│ └── media/ # Audio/keyboard native interfaces
├── native/ # Native components (Rust/Swift)
│ ├── audio-recorder/ # Audio capture (Rust)
│ ├── global-key-listener/ # Keyboard events (Rust)
│ ├── text-writer/ # Text insertion (Rust)
│ └── active-application/ # Get the active application for context (Rust)
├── server/ # gRPC transcription server
│ ├── src/ # Server implementation
│ └── infra/ # AWS infrastructure (CDK)
└── resources/ # Build resources & assets
```
### Available Scripts
```bash
# Development
bun run dev # Start with hot reload
bun run dev:rust # Build Rust components and start dev
# Building Native Components
bun run build:rust # Build for current platform
bun run build:rust:mac # Build for macOS (with universal binary)
bun run build:rust:win # Build for Windows
# Building Application
bun run build:mac # Build for macOS
bun run build:win # Build for Windows
./build-app.sh mac # Build macOS using build script
./build-app.sh windows # Build Windows using build script (requires Docker)
# Code Quality
bun run lint # Run ESLint
bun run format # Run Prettier
bun run lint:fix # Fix linting issues
```
---
## 🏗️ Architecture
### Client Architecture
**Ito** is built as a modern Electron application with a sophisticated multi-process architecture:
- **Main Process**: Handles system integration, permissions, and native component coordination
- **Renderer Process**: React-based UI with real-time audio visualization
- **Preload Scripts**: Secure IPC bridge between main and renderer processes
- **Native Components**: High-performance Rust binaries for audio capture and keyboard handling
### Technology Stack
**Frontend:**
- **Electron** - Cross-platform desktop framework
- **React 19** - Modern UI library with concurrent features
- **TypeScript** - Type-safe development
- **TailwindCSS** - Utility-first styling
- **Zustand** - Lightweight state management
- **Framer Motion** - Smooth animations
**Backend:**
- **Node.js** - Runtime environment
- **gRPC** - High-performance RPC for transcription services
- **SQLite** - Local data storage
- **Protocol Buffers** - Efficient data serialization
**Native Components:**
- **Rust** - System-level audio recording and keyboard event handling
- **Swift** - macOS-specific text manipulation and accessibility features
- **cpal** - Cross-platform audio library
- **enigo** - Cross-platform input simulation
**Infrastructure:**
- **AWS CDK** - Infrastructure as code
- **Docker** - Containerized deployments
- **Auth0** - Authentication and user management
### Communication Flow
```mermaid
graph TD
A[User Holds Trigger Key] --> B[Global Key Listener]
B --> C[Main Process]
C --> D[Audio Recorder Service]
D --> E[gRPC Transcription Service]
E --> F[AI Transcription Model]
F --> G[Transcribed Text]
G --> H[Text Writer Service]
H --> I[Active Text Field]
```
---
## 🔧 Configuration
### Keyboard Shortcuts
Customize your trigger keys in **Settings > Keyboard**:
- **Single key**: `Space`, `Fn`, etc.
- **Key combinations**: `Cmd + Space`, `Ctrl + Shift + V`, etc.
- **Complex shortcuts**: `Fn + Cmd + Space` for advanced workflows
### Audio Settings
Fine-tune audio capture in **Settings > Audio**:
- **Microphone selection**: Choose from available input devices
- **Sensitivity adjustment**: Optimize for your voice and environment
- **Noise reduction**: Filter background noise automatically
- **Audio feedback**: Enable/disable sound effects
### Privacy & Data
Control your data in **Settings > General**:
- **Local processing**: Keep voice data on your device
- **Cloud sync**: Synchronize settings across devices
- **Analytics**: Share anonymous usage data (optional)
- **Data export**: Download your notes and interaction history
---
## 🔒 Privacy & Security
### Data Handling
- **Local-enabled**: Voice processing can be done entirely on your device or using our cloud
- **Encrypted transmission**: All network communication uses TLS encryption
- **Minimal data collection**: Only essential data is processed and stored
- **User control**: Full control and transparency over data retention and deletion
### Permissions
**Ito** requires specific system permissions to function:
- **Microphone Access**: To capture your voice for transcription
- **Accessibility Access**: To detect keyboard shortcuts and insert text
- **Network Access**: For cloud features and updates (optional)
### Open Source
This project is open source under the GNU General Public License. You can:
- Audit the source code for security and privacy
- Contribute improvements and bug fixes
- Fork and customize for your specific needs
- Report security issues through responsible disclosure
---
## 🤝 Contributing
We welcome contributions! Whether you're fixing bugs, adding features, or improving documentation, your help makes **Ito** better for everyone.
### Getting Started
1. **Fork the repository** and clone your fork
2. **Create a feature branch** from `dev`
3. **Make your changes** with clear commit messages
4. **Test thoroughly** across supported platforms
5. **Submit a pull request** with a detailed description
### Development Guidelines
- **Code Style**: Use Prettier and ESLint configurations
- **Type Safety**: Maintain strong TypeScript typing
- **Testing**: Add tests for new features
- **Documentation**: Update docs for API changes
- **Performance**: Consider impact on time between recording and text insertion
### Areas for Contribution
- **Accuracy improvements**: Better transcription algorithms
- **Language support**: Additional language models
- **UI/UX enhancements**: Better user experience
- **Platform support**: Windows stability testing, Linux compatibility
- **Documentation**: Tutorials, guides, and examples
---
## 📄 License
This project is licensed under the **GNU General Public License** - see the [LICENSE](LICENSE) file for details.
---
## 🙏 Acknowledgments
**Ito** is built with and inspired by amazing open source projects:
- **[Electron React App](https://github.com/guasam/electron-react-app)** by @guasam - The foundational template that provided our modern Electron + React architecture
- **Electron** - Cross-platform desktop apps with web technologies
- **React** - Modern UI development
- **Rust** - Systems programming language for native components
- **gRPC** - High-performance RPC framework
- **TailwindCSS** - Utility-first CSS framework
---
## 📞 Support
- **Community**: [GitHub Discussions](https://github.com/heyito/ito/discussions)
- **Issues**: [GitHub Issues](https://github.com/heyito/ito/issues)
- **Website**: [heyito.ai](https://www.heyito.ai)
================================================
FILE: app/app.tsx
================================================
import { HashRouter, Routes, Route } from 'react-router-dom'
import appIcon from '@/resources/build/icon.png'
import HomeKit from '@/app/components/home/HomeKit'
import WelcomeKit from '@/app/components/welcome/WelcomeKit'
import Pill from '@/app/components/pill/Pill'
import {
STEP_NAMES,
STEP_NAMES_ARRAY,
useOnboardingStore,
} from '@/app/store/useOnboardingStore'
import { useAuth } from '@/app/components/auth/useAuth'
import { WindowContextProvider } from '@/lib/window'
import { Auth0Provider } from '@/app/components/auth/Auth0Provider'
import { useDeviceChangeListener } from './hooks/useDeviceChangeListener'
import { verifyStoredMicrophone } from './media/microphone'
import { useEffect } from 'react'
const MainApp = () => {
const { onboardingCompleted, onboardingStep } = useOnboardingStore()
const { isAuthenticated } = useAuth()
useDeviceChangeListener()
useEffect(() => {
verifyStoredMicrophone()
}, [])
const onboardingSetupCompleted =
onboardingStep >= STEP_NAMES_ARRAY.indexOf(STEP_NAMES.TRY_IT_OUT)
const shouldEnableShortcutGlobally =
onboardingCompleted || onboardingSetupCompleted
// If authenticated and onboarding completed, show main app
if (isAuthenticated && onboardingCompleted) {
window.api.send(
'electron-store-set',
'settings.isShortcutGloballyEnabled',
shouldEnableShortcutGlobally,
)
return
}
// If authenticated but onboarding not completed, continue onboarding
window.api.send(
'electron-store-set',
'settings.isShortcutGloballyEnabled',
shouldEnableShortcutGlobally,
)
return
}
export default function App() {
return (
{/* Route for the pill window */}
>
}
/>
{/* Default route for the main application window */}
>
}
/>
)
}
================================================
FILE: app/assets/.gitignore
================================================
================================================
FILE: app/components/analytics/index.ts
================================================
import posthog from 'posthog-js'
import log from 'electron-log'
import { STORE_KEYS } from '../../../lib/constants/store-keys'
import { v4 as uuidv4 } from 'uuid'
import type { OnboardingCategory } from '../../store/useOnboardingStore'
// Get or generate a machine-based device ID that's shared across all windows
const getSharedDeviceId = async (): Promise => {
try {
// Just request the device ID - main process handles generation/caching
const deviceId = await window.api?.invoke('analytics:get-device-id')
if (deviceId) {
console.log('[Analytics] Using machine-based device ID:', deviceId)
return deviceId
}
throw new Error('No device ID returned from main process')
} catch (error) {
log.error('[Analytics] Could not get machine device ID:', error)
// In true emergency, generate a temporary UUID as fallback
return uuidv4()
}
}
// Check if analytics should be enabled
const getAnalyticsEnabled = (): boolean => {
if (!import.meta.env.VITE_POSTHOG_API_KEY) {
console.warn('[Analytics] No PostHog API key found, analytics disabled')
return false
}
try {
const settings = window.electron?.store?.get(STORE_KEYS.SETTINGS)
return settings?.shareAnalytics ?? true
} catch (error) {
console.warn(
'[Analytics] Could not read settings, defaulting to enabled:',
error,
)
return true
}
}
const initPostHog = () => {
const isPill =
typeof window !== 'undefined' &&
typeof window.location !== 'undefined' &&
typeof window.location.hash === 'string' &&
window.location.hash.startsWith('#/pill')
posthog.init(import.meta.env.VITE_POSTHOG_API_KEY, {
api_host: import.meta.env.VITE_POSTHOG_HOST,
disable_session_recording: true,
disable_surveys: true,
advanced_disable_decide: true,
persistence: 'cookie',
// Disable default web auto-capture and pageviews for the pill window only
autocapture: !isPill,
capture_pageview: !isPill,
sanitize_properties: (props: Record) => {
const p = { ...props }
delete (p as any).$current_url
delete (p as any).$pathname
delete (p as any).$host
delete (p as any).$referrer
return p
},
})
}
// Initialize PostHog only if analytics is enabled
let isAnalyticsInitialized = false
let sharedDeviceId: string | null = null
const analyticsEnabled = getAnalyticsEnabled()
console.log('[Analytics] Analytics enabled:', analyticsEnabled)
// Initialize PostHog asynchronously
const initializeAnalytics = async () => {
if (!analyticsEnabled) {
console.log('[Analytics] PostHog disabled by user settings')
return
}
try {
sharedDeviceId = await getSharedDeviceId()
console.log('[Analytics] Using shared device ID:', sharedDeviceId)
initPostHog()
if (sharedDeviceId) {
posthog.register({ device_id: sharedDeviceId })
}
// Attempt to resolve and alias install token to website distinct id
try {
const result = await window.api?.invoke('analytics:resolve-install-token')
if (result && result.success && result.websiteDistinctId) {
try {
posthog.alias(result.websiteDistinctId)
console.log(
'[Analytics] Aliased to website distinct_id from install token',
)
} catch (aliasErr) {
log.warn('[Analytics] alias() failed:', aliasErr)
}
}
} catch (err) {
log.warn('[Analytics] resolve-install-token failed:', err)
}
isAnalyticsInitialized = true
// Update the service instance after successful initialization
analytics.updateInitializationStatus(true, sharedDeviceId)
console.log(
'[Analytics] PostHog initialized with shared device ID:',
sharedDeviceId,
)
} catch (error) {
log.error('[Analytics] Failed to initialize analytics:', error)
}
}
// Initialize analytics when the module loads
initializeAnalytics()
// Event types for type safety
export interface BaseEventProperties {
timestamp?: string
session_id?: string
[key: string]: any
}
export interface OnboardingEventProperties extends BaseEventProperties {
step: number
step_name: string
category: OnboardingCategory
total_steps: number
referral_source?: string
provider?: string
}
export interface HotkeyEventProperties extends BaseEventProperties {
action: 'press' | 'release'
keys: string[]
duration_ms?: number
session_duration_ms?: number
}
export interface AuthEventProperties extends BaseEventProperties {
provider: string
is_returning_user: boolean
user_id?: string
}
export interface SettingsEventProperties extends BaseEventProperties {
setting_name: string
old_value: any
new_value: any
setting_category: string
}
export interface UserProperties {
user_id: string
email?: string
name?: string
provider?: string
created_at?: string
last_active?: string
onboarding_completed?: boolean
referral_source?: string
keyboard_shortcuts?: string[]
}
// Event constants
export const ANALYTICS_EVENTS = {
// Onboarding events
ONBOARDING_STARTED: 'onboarding_started',
ONBOARDING_STEP_COMPLETED: 'onboarding_step_completed',
ONBOARDING_STEP_VIEWED: 'onboarding_step_viewed',
ONBOARDING_COMPLETED: 'onboarding_completed',
ONBOARDING_ABANDONED: 'onboarding_abandoned',
// Authentication events
AUTH_SIGNUP_STARTED: 'auth_signup_started',
AUTH_SIGNUP_COMPLETED: 'auth_signup_completed',
AUTH_SIGNIN_STARTED: 'auth_signin_started',
AUTH_SIGNIN_COMPLETED: 'auth_signin_completed',
AUTH_SIGNIN_FAILED: 'auth_signin_failed',
AUTH_LOGOUT: 'auth_logout',
AUTH_LOGOUT_FAILED: 'auth_logout_failed',
AUTH_STATE_GENERATION_FAILED: 'auth_state_generation_failed',
AUTH_METHOD_FAILED: 'auth_method_failed',
// Recording events
RECORDING_STARTED: 'recording_started',
RECORDING_COMPLETED: 'recording_completed',
MANUAL_RECORDING_STARTED: 'manual_recording_started',
MANUAL_RECORDING_COMPLETED: 'manual_recording_completed',
MANUAL_RECORDING_ABANDONED: 'manual_recording_abandoned',
// Settings events
SETTING_CHANGED: 'setting_changed',
MICROPHONE_CHANGED: 'microphone_changed',
KEYBOARD_SHORTCUTS_CHANGED: 'keyboard_shortcuts_changed',
} as const
export type AnalyticsEvent =
(typeof ANALYTICS_EVENTS)[keyof typeof ANALYTICS_EVENTS]
/**
* Professional Analytics Service for Ito
* Handles all analytics tracking with proper typing and error handling
*/
class AnalyticsService {
private isInitialized: boolean = isAnalyticsInitialized
private currentUserId: string | null = null
private currentProvider: string | null = null
private sessionStartTime: number = Date.now()
private deviceId: string | null = null
constructor() {
// Device ID will be set after async initialization
this.deviceId = sharedDeviceId
console.log(
`[Analytics] Service initialized (enabled: ${this.isInitialized}, deviceId: ${this.deviceId || 'pending'})`,
)
}
/**
* Enable analytics (re-initialize if needed)
*/
async enableAnalytics() {
if (!this.isInitialized && import.meta.env.VITE_POSTHOG_API_KEY) {
try {
const deviceId = await getSharedDeviceId()
this.deviceId = deviceId
initPostHog()
if (deviceId) {
posthog.register({ device_id: deviceId })
}
this.isInitialized = true
console.log(
'[Analytics] Analytics enabled and initialized with shared device ID:',
deviceId,
)
} catch (error) {
log.error('[Analytics] Failed to enable analytics:', error)
}
}
}
/**
* Disable analytics
*/
disableAnalytics() {
this.isInitialized = false
this.currentUserId = null
this.currentProvider = null
try {
posthog.opt_out_capturing()
} catch (error) {
log.warn('[Analytics] Failed to opt-out capturing:', error)
}
console.log('[Analytics] Analytics disabled')
}
/**
* Check if analytics is currently enabled
*/
isEnabled(): boolean {
return this.isInitialized
}
/**
* Set user identification and properties
*/
identifyUser(
userId: string,
properties: Partial = {},
provider?: string,
) {
console.log('identifyUser', userId, properties, provider)
// Store provider information
if (provider) {
this.currentProvider = provider
}
if (!this.shouldTrack()) {
console.log(
'[Analytics] User identification skipped - analytics disabled or self-hosted user',
)
return
}
try {
if (this.currentUserId !== userId) {
this.currentUserId = userId
const props = {
user_id: userId,
last_active: new Date().toISOString(),
...properties,
}
posthog.identify(userId, props)
console.log(
`[Analytics] User identified: ${userId} (deviceId: ${this.deviceId || 'pending'})`,
)
} else if (Object.keys(properties).length > 0) {
posthog.identify(undefined, {
...properties,
last_active: new Date().toISOString(),
})
}
} catch (error) {
log.error('[Analytics] Failed to identify user:', error)
}
}
/**
* Update user properties
*/
updateUserProperties(properties: Partial) {
if (!this.shouldTrack() || !this.currentUserId) {
console.log(
'[Analytics] User properties update skipped - analytics disabled, self-hosted user, or user not identified',
)
return
}
try {
posthog.identify(undefined, properties)
console.log('[Analytics] User properties updated')
} catch (error) {
log.error('[Analytics] Failed to update user properties:', error)
}
}
/**
* Track a generic event
*/
track(eventName: AnalyticsEvent, properties: BaseEventProperties = {}) {
if (!this.shouldTrack()) {
return
}
try {
const eventProperties = {
timestamp: new Date().toISOString(),
session_duration_ms: Date.now() - this.sessionStartTime,
...properties,
}
posthog.capture(eventName, {
...eventProperties,
...(this.currentUserId ? { user_id: this.currentUserId } : {}),
})
console.log(
`[Analytics] Event tracked: ${eventName} (deviceId: ${this.deviceId || 'pending'}, userId: ${this.currentUserId || 'anonymous'})`,
)
} catch (error) {
log.error(`[Analytics] Failed to track event ${eventName}:`, error)
}
}
/**
* Track onboarding events
*/
trackOnboarding(
eventName: Extract<
AnalyticsEvent,
| 'onboarding_started'
| 'onboarding_step_completed'
| 'onboarding_step_viewed'
| 'onboarding_completed'
| 'onboarding_abandoned'
>,
properties: OnboardingEventProperties,
) {
console.log('trackOnboarding', eventName, properties)
this.track(eventName, properties)
}
/**
* Track authentication events
*/
trackAuth(
eventName: Extract<
AnalyticsEvent,
| 'auth_signup_started'
| 'auth_signup_completed'
| 'auth_signin_started'
| 'auth_signin_completed'
| 'auth_logout'
>,
properties: AuthEventProperties,
) {
this.track(eventName, properties)
}
/**
* Track settings changes
*/
trackSettings(
eventName: Extract<
AnalyticsEvent,
| 'setting_changed'
| 'microphone_changed'
| 'keyboard_shortcut_changed'
| 'privacy_mode_toggled'
| 'keyboard_shortcuts_changed'
>,
properties: SettingsEventProperties,
) {
this.track(eventName, properties)
}
/**
* Track permission events
*/
trackPermission(
eventName: Extract<
AnalyticsEvent,
'permission_requested' | 'permission_granted' | 'permission_denied'
>,
permissionType: 'microphone' | 'accessibility',
properties: BaseEventProperties = {},
) {
this.track(eventName, {
permission_type: permissionType,
...properties,
})
}
/**
* Reset analytics (for logout)
*/
resetUser() {
if (!this.isInitialized) {
console.log('[Analytics] User reset skipped - analytics disabled')
return
}
try {
posthog.reset()
this.currentUserId = null
this.currentProvider = null
console.log('[Analytics] User session reset')
} catch (error) {
log.error('[Analytics] Failed to reset user session:', error)
}
}
/**
* Get current session duration
*/
getSessionDuration(): number {
return Date.now() - this.sessionStartTime
}
/**
* Check if user is identified
*/
isUserIdentified(): boolean {
return this.currentUserId !== null
}
/**
* Get the current device ID
*/
getDeviceId(): string | null {
return this.deviceId
}
/**
* Update initialization status (called after async initialization completes)
*/
updateInitializationStatus(isInitialized: boolean, deviceId: string | null) {
this.isInitialized = isInitialized
this.deviceId = deviceId
console.log(
`[Analytics] Service status updated (enabled: ${this.isInitialized}, deviceId: ${this.deviceId})`,
)
}
/**
* Check if analytics should be tracked based on provider
*/
private shouldTrack(): boolean {
if (!this.isInitialized) {
return false
}
// Skip tracking for self-hosted users
if (this.currentProvider === 'self-hosted') {
console.log('[Analytics] Tracking skipped - self-hosted user')
return false
}
return true
}
}
// Export singleton instance
export const analytics = new AnalyticsService()
// Function to update analytics based on settings change
export const updateAnalyticsFromSettings = (shareAnalytics: boolean) => {
if (shareAnalytics && !analytics.isEnabled()) {
analytics.enableAnalytics()
console.log('[Analytics] Analytics enabled by settings change')
} else if (!shareAnalytics && analytics.isEnabled()) {
analytics.disableAnalytics()
console.log('[Analytics] Analytics disabled by settings change')
}
}
// Export convenience functions
export const trackEvent = analytics.track.bind(analytics)
export const identifyUser = analytics.identifyUser.bind(analytics)
export const updateUserProperties =
analytics.updateUserProperties.bind(analytics)
export const resetAnalytics = analytics.resetUser.bind(analytics)
================================================
FILE: app/components/auth/Auth0Provider.tsx
================================================
import React from 'react'
import { Auth0Provider as Auth0ReactProvider } from '@auth0/auth0-react'
import { Auth0Config, validateAuth0Config } from '../../../lib/auth/config'
interface Auth0ProviderProps {
children: React.ReactNode
}
export const Auth0Provider: React.FC = ({ children }) => {
// Validate configuration on startup
React.useEffect(() => {
try {
validateAuth0Config()
} catch (error) {
console.error('Auth0 configuration error:', error)
}
}, [])
return (
{children}
)
}
export default Auth0Provider
================================================
FILE: app/components/auth/useAuth.ts
================================================
import { useAuth0 } from '@auth0/auth0-react'
import { useCallback, useEffect, useMemo } from 'react'
import { Auth0Connections, Auth0Config } from '../../../lib/auth/config'
import { useAuthStore } from '../../store/useAuthStore'
import { type AuthUser, type AuthTokens } from '../../../lib/main/store'
import { useMainStore } from '@/app/store/useMainStore'
import { analytics, ANALYTICS_EVENTS } from '../analytics'
import { STORE_KEYS } from '../../../lib/constants/store-keys'
import { useOnboardingStore } from '@/app/store/useOnboardingStore'
export const useAuth = () => {
const {
logout,
user,
isAuthenticated: auth0IsAuthenticated,
isLoading: auth0IsLoading,
error: auth0Error,
getAccessTokenSilently,
getIdTokenClaims,
} = useAuth0()
// Get auth state from our store
const {
isAuthenticated: storeIsAuthenticated,
user: storeUser,
tokens,
isLoading: storeIsLoading,
error: storeError,
clearAuth,
setSelfHostedMode,
} = useAuthStore()
// Combine Auth0 and store state - prioritize store state for external auth
const isAuthenticated = storeIsAuthenticated || auth0IsAuthenticated
const isLoading = storeIsLoading || auth0IsLoading
const error = storeError || auth0Error
// Convert Auth0 user to our user interface
const auth0User: AuthUser | null = useMemo(() => {
if (!user) return null
return {
id: user.sub || '',
email: user.email,
name: user.name,
picture: user.picture,
provider: user.sub?.includes('|') ? user.sub.split('|')[0] : 'unknown',
lastSignInAt: new Date().toISOString(), // Only updated when user object changes
}
}, [user]) // Dependency array now correctly includes 'user'
// Prioritize store user over Auth0 user
const authUser = storeUser || auth0User
// Hydrate per-user onboarding state helper
async function hydrateOnboardingState(context?: string): Promise {
try {
const saved = await window.api.getOnboardingState?.()
const onboarding = useOnboardingStore.getState()
if (saved?.onboardingCompleted) {
onboarding.setOnboardingCompleted()
} else {
onboarding.resetOnboarding()
onboarding.incrementOnboardingStep()
}
} catch (e) {
const suffix = context ? ` (${context})` : ''
console.warn(`[useAuth] Failed to hydrate onboarding state${suffix}:`, e)
}
}
// Check for token expiration on startup
useEffect(() => {
// Check if we have valid auth state stored
const storedAuth = window.electron?.store?.get(STORE_KEYS.AUTH)
const hasStoredTokens = storedAuth?.tokens?.access_token
// Also check main store for tokens (backwards compatibility)
const hasMainStoreToken = window.electron?.store?.get(
STORE_KEYS.ACCESS_TOKEN,
)
if ((hasStoredTokens || hasMainStoreToken) && !isAuthenticated) {
console.log('Detected expired tokens on startup, clearing auth state')
// Clear any remaining auth data
clearAuth(true)
// Track the automatic logout
const currentUser = authUser
if (currentUser) {
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_LOGOUT, {
provider: currentUser.provider || 'unknown',
is_returning_user: true,
user_id: currentUser.id,
complete_signout: false,
session_duration_ms: analytics.getSessionDuration(),
reason: 'token_expired_startup',
})
}
}
}, [isAuthenticated, authUser, clearAuth])
useEffect(() => {
if (authUser) {
analytics.identifyUser(
authUser.id,
{
user_id: authUser.id,
email: authUser.email,
name: authUser.name,
provider: authUser.provider,
created_at: authUser.lastSignInAt,
},
authUser.provider,
)
// Notify pill window of user authentication
if (window.api?.notifyUserAuthUpdate) {
window.api.notifyUserAuthUpdate({
id: authUser.id,
email: authUser.email,
name: authUser.name,
provider: authUser.provider,
})
}
} else {
// Notify pill window that user is not authenticated (logout/reset)
if (window.api?.notifyUserAuthUpdate) {
console.log('[useAuth] Notifying pill window of user reset')
window.api.notifyUserAuthUpdate(null)
}
}
}, [
authUser,
auth0IsAuthenticated,
auth0User,
storeIsAuthenticated,
storeUser,
])
// Handle auth code from protocol URL - only set up listener once globally
useEffect(() => {
if (!window.api?.on) {
console.warn('window.api.on not available')
return
}
// Check if listener is already set up
if ((window as any).__authCodeListenerSetup) {
return
}
// Mark that we've set up the listener
;(window as any).__authCodeListenerSetup = true
const cleanup = window.api.on(
'auth-code-received',
async (authCode: string, state: string) => {
try {
// Exchange authorization code for tokens via main process
const result = await window.api.invoke('exchange-auth-code', {
authCode,
state,
config: Auth0Config,
})
if (!result.success) {
throw new Error(result.error)
}
// Store tokens and user info in the auth store
if (result.tokens && result.userInfo) {
// Extract provider from Auth0 user ID (format: "provider|id")
const providerId = result.userInfo.id || ''
const provider = providerId.includes('|')
? providerId.split('|')[0]
: 'unknown'
// Check if this is a returning user
const existingUser = useAuthStore.getState().user
const isReturningUser =
!!existingUser && existingUser.id === result.userInfo.id
useAuthStore
.getState()
.setAuthData(
result.tokens as AuthTokens,
result.userInfo as AuthUser,
provider,
)
// Track successful signin
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {
provider,
is_returning_user: isReturningUser,
user_id: result.userInfo.id,
})
useMainStore.getState().setCurrentPage('home')
await window.api.notifyLoginSuccess(
result.userInfo,
result.tokens.id_token,
result.tokens.access_token,
)
// Hydrate per-user onboarding state from SQLite for the now-authenticated user
await hydrateOnboardingState()
} else {
throw new Error('Missing tokens or user info in response')
}
} catch (error) {
console.error('Error handling auth code from protocol URL:', error)
// Track signin failure
analytics.track(ANALYTICS_EVENTS.AUTH_SIGNIN_FAILED, {
error_message:
error instanceof Error ? error.message : 'Unknown error',
auth_method: 'external_browser',
})
alert(
`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
},
)
return () => {
cleanup()
;(window as any).__authCodeListenerSetup = false
}
}, [])
// External browser authentication - now the primary method
const openExternalAuth = useCallback(
async (
connection?: string,
options?: { email?: string; mode?: 'login' | 'signup' },
) => {
// Track signin attempt started
const provider = connection || 'unknown'
const eventType =
options?.mode === 'signup'
? ANALYTICS_EVENTS.AUTH_SIGNUP_STARTED
: ANALYTICS_EVENTS.AUTH_SIGNIN_STARTED
analytics.trackAuth(eventType, {
provider,
is_returning_user: false, // We don't know yet
auth_method: 'external_browser',
})
let authState = useAuthStore.getState().state
// Generate new auth state if not available
if (!authState) {
try {
// Generate fresh auth state from the main process
authState = await window.api.generateNewAuthState()
if (!authState) {
throw new Error('Generated auth state is null')
}
// Update the store with the new auth state
useAuthStore.getState().updateState(authState)
} catch (error) {
console.error('Failed to generate new auth state:', error)
// Track auth state generation failure
analytics.track(ANALYTICS_EVENTS.AUTH_STATE_GENERATION_FAILED, {
provider,
error_message:
error instanceof Error ? error.message : 'Unknown error',
})
throw new Error(
'Failed to generate auth state. Please restart the app.',
)
}
}
const params = new URLSearchParams({
response_type: 'code',
client_id: Auth0Config.clientId,
redirect_uri: Auth0Config.redirectUri,
scope: Auth0Config.scope,
prompt: 'select_account',
state: authState.state,
code_challenge: authState.codeChallenge,
code_challenge_method: 'S256',
})
// Add audience if configured
if (Auth0Config.audience) {
params.append('audience', Auth0Config.audience)
}
if (connection) {
params.append('connection', connection)
}
if (options?.email) {
params.append('login_hint', options.email)
}
if (options?.mode === 'signup') {
params.append('screen_hint', 'signup')
}
const authUrl = `https://${Auth0Config.domain}/authorize?${params.toString()}`
// Open in external browser
if (window.api?.invoke) {
await window.api.invoke('web-open-url', authUrl)
} else {
window.open(authUrl, '_blank')
}
},
[],
)
// Helper function to reduce duplication in social auth methods
const createSocialAuthMethod = useCallback(
(connection: string, providerName: string) => {
return async (email?: string) => {
try {
await openExternalAuth(connection, email ? { email } : undefined)
} catch (error) {
console.error(`${providerName} external auth failed:`, error)
// Track auth method failure
analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {
provider: providerName.toLowerCase(),
error_message:
error instanceof Error ? error.message : 'Unknown error',
auth_method: 'external_browser',
})
throw error
}
}
},
[openExternalAuth],
)
// Social authentication methods - now use external browser by default
const loginWithGoogle = createSocialAuthMethod(
Auth0Connections.google,
'Google',
)
const loginWithMicrosoft = createSocialAuthMethod(
Auth0Connections.microsoft,
'Microsoft',
)
const loginWithApple = createSocialAuthMethod(Auth0Connections.apple, 'Apple')
// GitHub authentication - now uses external browser
const loginWithGitHub = createSocialAuthMethod(
Auth0Connections.github,
'GitHub',
)
// Email/password via Auth0 Database connection
const loginWithEmail = useCallback(
async (email?: string) => {
try {
await openExternalAuth(
Auth0Connections.database,
email ? { email, mode: 'login' } : { mode: 'login' },
)
} catch (error) {
console.error('Email login failed:', error)
analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {
provider: 'email',
error_message:
error instanceof Error ? error.message : 'Unknown error',
auth_method: 'external_browser',
})
throw error
}
},
[openExternalAuth],
)
// Direct email/password login without opening a browser window
const loginWithEmailPassword = useCallback(
async (
email: string,
password: string,
options?: { skipNavigate?: boolean },
) => {
try {
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_STARTED, {
provider: 'email',
is_returning_user: false,
auth_method: 'password_realm',
})
const result = await window.api.invoke('auth0-db-login', {
email,
password,
})
if (!result?.success || !result?.tokens) {
throw new Error(result?.error || 'Login failed')
}
const tokens = result.tokens as AuthTokens
const idToken = tokens.id_token || ''
let userInfo: AuthUser | null = null
try {
const payloadPart = idToken.split('.')[1]
const base64 = payloadPart.replace(/-/g, '+').replace(/_/g, '/')
const padded = base64 + '==='.slice((base64.length + 3) % 4)
const json = atob(padded)
const payload = JSON.parse(json)
userInfo = {
id: payload.sub || '',
email: payload.email,
name: payload.name,
picture: payload.picture,
provider:
typeof payload.sub === 'string' && payload.sub.includes('|')
? payload.sub.split('|')[0]
: 'email',
lastSignInAt: new Date().toISOString(),
}
} catch {
console.warn(
'Failed to decode id_token, proceeding with minimal profile',
)
userInfo = {
id: 'email',
email,
provider: 'email',
lastSignInAt: new Date().toISOString(),
}
}
useAuthStore
.getState()
.setAuthData(tokens, userInfo as AuthUser, 'email')
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {
provider: 'email',
is_returning_user: false,
user_id: userInfo?.id,
})
if (!options?.skipNavigate) {
useMainStore.getState().setCurrentPage('home')
}
await window.api.notifyLoginSuccess(
userInfo,
tokens.id_token ?? null,
tokens.access_token ?? null,
)
// Hydrate per-user onboarding state
await hydrateOnboardingState('email/password')
} catch (error) {
console.error('Email/password login failed:', error)
analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {
provider: 'email',
error_message:
error instanceof Error ? error.message : 'Unknown error',
auth_method: 'password_realm',
})
throw error
}
},
[],
)
const signupWithEmail = useCallback(
async (email?: string) => {
try {
await openExternalAuth(
Auth0Connections.database,
email ? { email, mode: 'signup' } : { mode: 'signup' },
)
} catch (error) {
console.error('Email signup failed:', error)
analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {
provider: 'email',
error_message:
error instanceof Error ? error.message : 'Unknown error',
auth_method: 'external_browser',
})
throw error
}
},
[openExternalAuth],
)
// Directly create a database user via Auth0 Authentication API (proxied via main to avoid CORS)
const createDatabaseUser = useCallback(
async (email: string, password: string, name: string) => {
const result = await window.api.invoke('auth0-db-signup', {
email,
password,
name,
})
if (!result?.success) {
throw new Error(result?.error || 'Signup failed')
}
return result.data
},
[],
)
// Self-hosted authentication - bypasses all external auth
const loginWithSelfHosted = useCallback(async () => {
try {
// Track self-hosted signin attempt
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_STARTED, {
provider: 'self-hosted',
is_returning_user: false,
auth_method: 'self_hosted',
})
setSelfHostedMode()
// Notify main process about self-hosted login and wait for it to complete
const selfHostedProfile = {
id: 'self-hosted',
provider: 'self-hosted',
lastSignInAt: new Date().toISOString(),
}
await window.api.notifyLoginSuccess(
selfHostedProfile,
null, // No idToken for self-hosted
null, // No accessToken for self-hosted
)
// Hydrate per-user onboarding state
await hydrateOnboardingState('self-hosted')
// Track successful self-hosted signin
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {
provider: 'self-hosted',
is_returning_user: false,
user_id: 'self-hosted',
auth_method: 'self_hosted',
})
} catch (error) {
console.error('Self-hosted mode activation error:', error)
// Track self-hosted signin failure
analytics.track(ANALYTICS_EVENTS.AUTH_SIGNIN_FAILED, {
provider: 'self-hosted',
error_message: error instanceof Error ? error.message : 'Unknown error',
auth_method: 'self_hosted',
})
throw error
}
}, [setSelfHostedMode])
// Get access token for API calls
const getAccessToken = useCallback(async () => {
try {
// First try to get from our store (for external auth)
if (tokens?.access_token) {
return tokens.access_token
}
// Fallback to Auth0 silent auth (for popup/redirect auth)
return await getAccessTokenSilently()
} catch (error) {
console.error('Error getting access token:', error)
throw error
}
}, [getAccessTokenSilently, tokens])
// Manual token refresh
const refreshTokens = useCallback(async () => {
try {
console.log('Manually refreshing tokens...')
const result = await window.api.invoke('refresh-tokens')
if (result.success) {
console.log('Manual token refresh successful')
return result
} else {
console.error('Manual token refresh failed:', result.error)
throw new Error(result.error)
}
} catch (error) {
console.error('Error during manual token refresh:', error)
throw error
}
}, [])
// Logout
const logoutUser = useCallback(
async (completelySignOut: boolean = false) => {
try {
// Track logout attempt
const currentUser = authUser
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_LOGOUT, {
provider: currentUser?.provider || 'unknown',
is_returning_user: true, // If they're logging out, they were logged in
user_id: currentUser?.id,
complete_signout: completelySignOut,
session_duration_ms: analytics.getSessionDuration(),
})
// Clear main process store first
await window.api.logout()
// Clear our auth store, preserving user data by default
clearAuth(!completelySignOut)
// Also logout from Auth0 if using Auth0 session
if (auth0IsAuthenticated) {
logout({
logoutParams: {
returnTo: window.location.origin,
},
})
}
} catch (error) {
console.error('Error during logout:', error)
// Track logout failure
analytics.track(ANALYTICS_EVENTS.AUTH_LOGOUT_FAILED, {
provider: authUser?.provider || 'unknown',
error_message:
error instanceof Error ? error.message : 'Unknown error',
complete_signout: completelySignOut,
})
// Still try to clear local auth even if main process logout fails
clearAuth(!completelySignOut)
}
},
[logout, clearAuth, auth0IsAuthenticated, authUser],
)
// Handle auth token events from main process
useEffect(() => {
if (!window.api?.on) {
console.warn('window.api.on not available')
return
}
// Check if listener is already set up
if ((window as any).__authTokenListenerSetup) {
return
}
// Mark that we've set up the listener
;(window as any).__authTokenListenerSetup = true
// Handle token refresh success
const cleanupTokensRefreshed = window.api.on(
'tokens-refreshed',
async (newTokens: AuthTokens) => {
console.log('Tokens refreshed successfully, updating auth store')
try {
// Update the auth store with refreshed tokens
const currentUser = authUser
if (currentUser) {
useAuthStore
.getState()
.setAuthData(newTokens, currentUser, currentUser.provider)
// Track successful token refresh
analytics.track(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {
provider: currentUser.provider || 'unknown',
user_id: currentUser.id,
is_returning_user: true,
reason: 'token_refresh',
})
}
} catch (error) {
console.error(
'Error updating auth store with refreshed tokens:',
error,
)
}
},
)
// Handle token expiration (when refresh fails or no refresh token available)
const cleanupTokenExpired = window.api.on(
'auth-token-expired',
async () => {
console.log('Auth token expired, automatically signing out user')
try {
// Track automatic logout due to token expiration
const currentUser = authUser
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_LOGOUT, {
provider: currentUser?.provider || 'unknown',
is_returning_user: true,
user_id: currentUser?.id,
complete_signout: false,
session_duration_ms: analytics.getSessionDuration(),
reason: 'token_expired',
})
logoutUser(false)
// Auth state will automatically redirect to welcome page
} catch (error) {
console.error('Error during automatic logout:', error)
// Still try to clear local auth even if main process logout fails
clearAuth(true)
}
},
)
return () => {
cleanupTokensRefreshed()
cleanupTokenExpired()
;(window as any).__authTokenListenerSetup = false
}
}, [logout, clearAuth, auth0IsAuthenticated, authUser, logoutUser])
return {
// Auth state
user: authUser,
isAuthenticated,
isLoading,
error,
// Authentication methods
loginWithGoogle,
loginWithMicrosoft,
loginWithApple,
loginWithGitHub,
loginWithEmail,
loginWithEmailPassword,
signupWithEmail,
createDatabaseUser,
loginWithSelfHosted,
logoutUser,
// Utilities
getAccessToken,
getIdTokenClaims,
refreshTokens,
}
}
================================================
FILE: app/components/home/HomeKit.tsx
================================================
import {
Home,
BookOpen,
FileText,
CogFour,
InfoCircle,
} from '@mynaui/icons-react'
import { ItoIcon } from '../icons/ItoIcon'
import { useMainStore } from '@/app/store/useMainStore'
import { useUserMetadataStore } from '@/app/store/useUserMetadataStore'
import { useOnboardingStore } from '@/app/store/useOnboardingStore'
import { useAuth } from '@/app/components/auth/useAuth'
import useBillingState from '@/app/hooks/useBillingState'
import { PaidStatus } from '@/lib/main/sqlite/models'
import { useEffect, useState, useRef } from 'react'
import { NavItem } from '../ui/nav-item'
import HomeContent from './contents/HomeContent'
import DictionaryContent from './contents/DictionaryContent'
import NotesContent from './contents/NotesContent'
import SettingsContent from './contents/SettingsContent'
import AboutContent from './contents/AboutContent'
export default function HomeKit() {
const { navExpanded, currentPage, setCurrentPage } = useMainStore()
const { metadata } = useUserMetadataStore()
const { onboardingCompleted } = useOnboardingStore()
const { isAuthenticated, user } = useAuth()
const billingState = useBillingState()
const [showText, setShowText] = useState(navExpanded)
const hasStartedTrialRef = useRef(false)
const previousUserIdRef = useRef(undefined)
const [isStartingTrial, setIsStartingTrial] = useState(false)
const isPro =
metadata?.paid_status === PaidStatus.PRO ||
metadata?.paid_status === PaidStatus.PRO_TRIAL ||
billingState.proStatus === 'active_pro' ||
billingState.proStatus === 'free_trial'
// Reset flags when user changes
useEffect(() => {
const currentUserId = user?.id
const previousUserId = previousUserIdRef.current
if (currentUserId && currentUserId !== previousUserId) {
// User changed - reset trial start flag
hasStartedTrialRef.current = false
setIsStartingTrial(false)
previousUserIdRef.current = currentUserId
} else if (currentUserId && previousUserId === undefined) {
// First time setting userId
previousUserIdRef.current = currentUserId
}
}, [user?.id])
// Start trial for users who don't have one yet
// Case 1: New users after onboarding completes
// Case 2: Existing users who completed onboarding but haven't started trial yet
useEffect(() => {
// Skip if still loading billing state or not authenticated
if (billingState.isLoading || !isAuthenticated) return
// Only proceed if onboarding is completed
if (!onboardingCompleted) return
// Check if user has a trial or subscription
const hasTrialOrSubscription =
billingState.proStatus === 'free_trial' ||
billingState.proStatus === 'active_pro' ||
isPro
// Start trial if:
// 1. User hasn't started trial yet (tracked by ref)
// 2. User doesn't have a trial or subscription
// 3. User has completed onboarding
if (!hasStartedTrialRef.current && !hasTrialOrSubscription) {
hasStartedTrialRef.current = true
setIsStartingTrial(true) // Set flag to indicate trial is being started
// Start trial
window.api.trial.startAfterOnboarding().catch(err => {
console.error('Failed to start trial:', err)
// Reset flag so we can retry if needed
hasStartedTrialRef.current = false
setIsStartingTrial(false)
})
}
}, [
onboardingCompleted,
isAuthenticated,
billingState.isLoading,
billingState.proStatus,
isPro,
])
// Listen for trial-started event to refresh billing state
useEffect(() => {
const offTrialStarted = window.api.on('trial-started', async () => {
// Trial started successfully - refresh billing state
// HomeContent will handle showing the dialog based on billing state transition
await billingState.refresh()
setIsStartingTrial(false) // Reset flag after trial starts
})
return () => {
offTrialStarted?.()
}
}, [billingState])
// Reset trial start flag when onboarding resets
useEffect(() => {
if (!onboardingCompleted) {
hasStartedTrialRef.current = false
setIsStartingTrial(false)
}
}, [onboardingCompleted])
// Listen for billing deep-link events and finalize subscription
useEffect(() => {
const offSuccess = window.api.on(
'billing-session-completed',
async (sessionId: string) => {
try {
if (sessionId) {
await window.api.billing.confirmSession(sessionId)
}
// Ensure trial is completed locally and on server
await window.api.trial.complete()
} catch (err) {
console.error('Failed to finalize billing session', err)
}
},
)
const offCancel = window.api.on('billing-session-cancelled', () => {
// No-op for now; could show a toast in the future
})
return () => {
offSuccess?.()
offCancel?.()
}
}, [])
// Handle text and positioning animation timing
useEffect(() => {
if (navExpanded) {
// When expanding: slide right first, then show text
const timer = setTimeout(() => {
setShowText(true) // Show text after slide starts
}, 75)
return () => clearTimeout(timer)
} else {
// When collapsing: hide text immediately, then center icons after slide completes
setShowText(false)
// Return no-op function
return () => {}
}
}, [navExpanded])
// Render the appropriate content based on current page
const renderContent = () => {
switch (currentPage) {
case 'home':
return
case 'dictionary':
return
case 'notes':
return
case 'settings':
return
case 'about':
return
default:
return
}
}
return (
}
title="Discord"
description="Join the community, share feedback, and grow with Ito."
buttonText="Join Discord"
onClick={handleDiscordClick}
/>
}
title="Team Call"
description="Got feedback or ideas? Book a quick call with the Ito team."
buttonText="Book a Call"
onClick={handleTeamCallClick}
/>
}
title="X (Twitter)"
description="Get updates, tips, and behind-the-scenes insights from the Ito team."
buttonText="Follow on X"
onClick={handleXClick}
/>
{/* Second Row: 2 items */}
}
title="GitHub"
description="Check out the code, contribute, or star the repo."
buttonText="View on GitHub"
onClick={handleGitHubClick}
/>
}
title="ito.ai"
description="Learn more about Ito, explore features, and see what's next."
buttonText="Go to Website"
onClick={handleWebsiteClick}
/>
)}
{/* Scroll to Top Button */}
{showScrollToTop && (
)}
{/* Status Indicator */}
{
setStatusIndicator(null)
setErrorMessage('')
setSuccessMessage('')
}}
successMessage={successMessage || 'Dictionary entry added successfully'}
errorMessage={errorMessage || 'Failed to add dictionary entry'}
/>
{/* Edit Entry Dialog */}
{/* Add New Entry Dialog */}
)
}
================================================
FILE: app/components/home/contents/HomeContent.tsx
================================================
import React, { useCallback, useEffect, useState } from 'react'
import {
ChartNoAxesColumn,
InfoCircle,
Play,
Stop,
Copy,
Check,
Download,
} from '@mynaui/icons-react'
import { EXTERNAL_LINKS } from '@/lib/constants/external-links'
import { useSettingsStore } from '../../../store/useSettingsStore'
import { Tooltip, TooltipTrigger, TooltipContent } from '../../ui/tooltip'
import { useAuthStore } from '@/app/store/useAuthStore'
import { Interaction } from '@/lib/main/sqlite/models'
import { TotalWordsIcon } from '../../icons/TotalWordsIcon'
import { SpeedIcon } from '../../icons/SpeedIcon'
import {
STREAK_MESSAGES,
SPEED_MESSAGES,
TOTAL_WORDS_MESSAGES,
getStreakLevel,
getSpeedLevel,
getTotalWordsLevel,
getActivityMessage,
} from './activityMessages'
import { ItoMode } from '@/app/generated/ito_pb'
import { getKeyDisplay } from '@/app/utils/keyboard'
import { createStereo48kWavFromMonoPCM } from '@/app/utils/audioUtils'
import { KeyName } from '@/lib/types/keyboard'
import { usePlatform } from '@/app/hooks/usePlatform'
import { ProUpgradeDialog } from '../ProUpgradeDialog'
import useBillingState from '@/app/hooks/useBillingState'
// Interface for interaction statistics
interface InteractionStats {
streakDays: number
totalWords: number
averageWPM: number
}
const StatCard = ({
title,
value,
description,
icon,
}: {
title: string
value: string
description: string
icon: React.ReactNode
}) => {
return (
{title}
{value}
{icon}
{description}
)
}
interface HomeContentProps {
isStartingTrial?: boolean
}
export default function HomeContent({
isStartingTrial = false,
}: HomeContentProps) {
const { getItoModeShortcuts } = useSettingsStore()
const keyboardShortcut = getItoModeShortcuts(ItoMode.TRANSCRIBE)[0].keys
const { user } = useAuthStore()
const firstName = user?.name?.split(' ')[0]
const platform = usePlatform()
const [interactions, setInteractions] = useState([])
const [loading, setLoading] = useState(true)
const [playingAudio, setPlayingAudio] = useState(null)
const [audioInstances, setAudioInstances] = useState<
Map
>(new Map())
const [copiedItems, setCopiedItems] = useState>(new Set())
const [openTooltipKey, setOpenTooltipKey] = useState(null)
const [stats, setStats] = useState({
streakDays: 0,
totalWords: 0,
averageWPM: 0,
})
const [showProDialog, setShowProDialog] = useState(false)
const billingState = useBillingState()
// Persist "has shown trial dialog" flag in electron-store to survive remounts
const [hasShownTrialDialog, setHasShownTrialDialogState] = useState(() => {
try {
const authStore = window.electron?.store?.get('auth') || {}
const value = authStore?.hasShownTrialDialog === true
return value
} catch {
return false
}
})
const setHasShownTrialDialog = useCallback((value: boolean) => {
try {
setHasShownTrialDialogState(value)
window.api.send('electron-store-set', 'auth.hasShownTrialDialog', value)
} catch {
console.warn('Failed to persist hasShownTrialDialog flag')
}
}, [])
// Show trial dialog when trial starts
useEffect(() => {
if (
billingState.isTrialActive &&
billingState.proStatus === 'free_trial' &&
!hasShownTrialDialog &&
!billingState.isLoading
) {
setShowProDialog(true)
setHasShownTrialDialog(true)
}
}, [
billingState.isTrialActive,
billingState.proStatus,
billingState.isLoading,
isStartingTrial,
hasShownTrialDialog,
setHasShownTrialDialog,
])
// Listen for trial start event to refresh billing state
useEffect(() => {
const offTrialStarted = window.api.on('trial-started', async () => {
await billingState.refresh()
})
const offBillingSuccess = window.api.on(
'billing-session-completed',
async () => {
await billingState.refresh()
},
)
return () => {
offTrialStarted?.()
offBillingSuccess?.()
}
}, [billingState])
// Reset dialog flag when trial is no longer active or user becomes pro
// Only reset if we're certain the trial has ended (not just during loading/refreshing)
useEffect(() => {
if (billingState.isLoading) {
// Don't reset during loading to avoid race conditions
return
}
const shouldReset =
billingState.proStatus === 'active_pro' ||
(billingState.proStatus === 'none' && !billingState.isTrialActive)
if (shouldReset && hasShownTrialDialog) {
setHasShownTrialDialog(false)
}
}, [
billingState.proStatus,
billingState.isTrialActive,
billingState.isLoading,
hasShownTrialDialog,
setHasShownTrialDialog,
])
// Calculate statistics from interactions
const calculateStats = useCallback(
(interactions: Interaction[]): InteractionStats => {
if (interactions.length === 0) {
return { streakDays: 0, totalWords: 0, averageWPM: 0 }
}
// Calculate streak (consecutive days with interactions)
const streakDays = calculateStreak(interactions)
// Calculate total words from transcripts
const totalWords = calculateTotalWords(interactions)
// Calculate average WPM (estimate based on average speaking rate)
const averageWPM = calculateAverageWPM(interactions)
return { streakDays, totalWords, averageWPM }
},
[],
)
const calculateStreak = (interactions: Interaction[]): number => {
if (interactions.length === 0) return 0
// Group interactions by date
const dateGroups = new Map()
interactions.forEach(interaction => {
const date = new Date(interaction.created_at).toDateString()
if (!dateGroups.has(date)) {
dateGroups.set(date, [])
}
dateGroups.get(date)!.push(interaction)
})
// Sort dates in descending order (most recent first)
const sortedDates = Array.from(dateGroups.keys()).sort(
(a, b) => new Date(b).getTime() - new Date(a).getTime(),
)
let streak = 0
const today = new Date()
for (let i = 0; i < sortedDates.length; i++) {
const currentDate = new Date(sortedDates[i])
const expectedDate = new Date(today)
expectedDate.setDate(today.getDate() - i)
// Check if current date matches expected date (allowing for today or previous consecutive days)
if (currentDate.toDateString() === expectedDate.toDateString()) {
streak++
} else {
break
}
}
return streak
}
const calculateTotalWords = (interactions: Interaction[]): number => {
return interactions.reduce((total, interaction) => {
const transcript = interaction.asr_output?.transcript?.trim()
if (transcript) {
// Count words by splitting on whitespace and filtering out empty strings
const words = transcript.split(/\s+/).filter(word => word.length > 0)
return total + words.length
}
return total
}, 0)
}
const calculateAverageWPM = (interactions: Interaction[]): number => {
const validInteractions = interactions.filter(
interaction =>
interaction.asr_output?.transcript?.trim() && interaction.duration_ms,
)
if (validInteractions.length === 0) return 0
let totalWords = 0
let totalDurationMs = 0
validInteractions.forEach(interaction => {
const transcript = interaction.asr_output?.transcript?.trim()
if (transcript && interaction.duration_ms) {
// Count words by splitting on whitespace and filtering out empty strings
const words = transcript.split(/\s+/).filter(word => word.length > 0)
totalWords += words.length
totalDurationMs += interaction.duration_ms
}
})
if (totalDurationMs === 0) return 0
// Calculate WPM: (total words / total duration in minutes)
const totalMinutes = totalDurationMs / (1000 * 60)
const wpm = totalWords / totalMinutes
// Round to nearest integer and ensure it's reasonable
return Math.round(Math.max(1, wpm))
}
const formatStreakText = (days: number): string => {
if (days === 0) return '0 days'
if (days === 1) return '1 day'
if (days < 7) return `${days} days`
if (days < 14) return '1 week'
if (days < 30) return `${Math.floor(days / 7)} weeks`
if (days < 60) return '1 month'
return `${Math.floor(days / 30)} months`
}
const loadInteractions = useCallback(async () => {
try {
const allInteractions = await window.api.interactions.getAll()
// Sort by creation date (newest first) - remove the slice(0, 10) to show all interactions
const sortedInteractions = allInteractions.sort(
(a: Interaction, b: Interaction) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
)
setInteractions(sortedInteractions)
// Calculate and set statistics
const calculatedStats = calculateStats(sortedInteractions)
setStats(calculatedStats)
} catch (error) {
console.error('Failed to load interactions:', error)
} finally {
setLoading(false)
}
}, [calculateStats])
useEffect(() => {
loadInteractions()
// Listen for new interactions
const handleInteractionCreated = () => {
loadInteractions()
}
const unsubscribe = window.api.on(
'interaction-created',
handleInteractionCreated,
)
// Cleanup listener on unmount
return unsubscribe
}, [loadInteractions])
// Cleanup audio instances on unmount
useEffect(() => {
return () => {
audioInstances.forEach(audio => {
try {
audio.pause()
audio.currentTime = 0
// Best-effort release of object URL if used
if (audio.src?.startsWith('blob:')) {
URL.revokeObjectURL(audio.src)
}
} catch {
/* ignore */
}
})
}
}, [audioInstances])
const formatTime = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
})
}
const formatDate = (dateString: string) => {
const date = new Date(dateString)
const today = new Date()
const yesterday = new Date()
yesterday.setDate(today.getDate() - 1)
const isToday = date.toDateString() === today.toDateString()
const isYesterday = date.toDateString() === yesterday.toDateString()
if (isToday) return 'TODAY'
if (isYesterday) return 'YESTERDAY'
return date
.toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric',
})
.toUpperCase()
}
const groupInteractionsByDate = (interactions: Interaction[]) => {
const groups: { [key: string]: Interaction[] } = {}
interactions.forEach(interaction => {
const dateKey = formatDate(interaction.created_at)
if (!groups[dateKey]) {
groups[dateKey] = []
}
groups[dateKey].push(interaction)
})
return groups
}
const getDisplayText = (interaction: Interaction) => {
// Check for errors first
if (interaction.asr_output?.error) {
// Prefer precise error code mapping when available
const code = interaction.asr_output?.errorCode
if (code === 'CLIENT_TRANSCRIPTION_QUALITY_ERROR') {
return {
text: 'Audio quality too low',
isError: true,
tooltip:
'Audio quality was too low to generate a reliable transcript',
}
}
if (
interaction.asr_output.error.includes('No speech detected in audio.') ||
interaction.asr_output.error.includes('Unable to transcribe audio.')
) {
return {
text: 'Audio is silent',
isError: true,
tooltip: "Ito didn't detect any words so the transcript is empty",
}
}
return {
text: 'Transcription failed',
isError: true,
tooltip: interaction.asr_output.error,
}
}
// Check for empty transcript
const transcript = interaction.asr_output?.transcript?.trim()
if (!transcript) {
return {
text: 'Audio is silent.',
isError: true,
tooltip: "Ito didn't detect any words so the transcript is empty",
}
}
// Return the actual transcript
return {
text: transcript,
isError: false,
tooltip: null,
}
}
const handleAudioPlayStop = async (interaction: Interaction) => {
try {
// If this interaction is currently playing, stop it
if (playingAudio === interaction.id) {
const current = audioInstances.get(interaction.id)
if (current) {
current.pause()
current.currentTime = 0
if (current.src?.startsWith('blob:')) {
URL.revokeObjectURL(current.src)
}
}
setPlayingAudio(null)
return
}
// Stop any other playing audio
if (playingAudio) {
const other = audioInstances.get(playingAudio)
if (other) {
other.pause()
other.currentTime = 0
if (other.src?.startsWith('blob:')) {
URL.revokeObjectURL(other.src)
}
}
}
if (!interaction.raw_audio) {
console.warn('No audio data available for this interaction')
return
}
// Set playing state immediately for responsive UI
setPlayingAudio(interaction.id)
// Reuse existing audio instance if available
let audio = audioInstances.get(interaction.id)
if (!audio) {
const pcmData = new Uint8Array(interaction.raw_audio)
try {
// Convert raw PCM (mono, typically 16 kHz) to 48 kHz stereo WAV for smoother playback
const wavBuffer = createStereo48kWavFromMonoPCM(
pcmData,
interaction.sample_rate || 16000,
48000,
)
const audioBlob = new Blob([wavBuffer], { type: 'audio/wav' })
const audioUrl = URL.createObjectURL(audioBlob)
audio = new Audio(audioUrl)
audio.onended = () => {
setPlayingAudio(null)
if (audio && audio.src?.startsWith('blob:')) {
URL.revokeObjectURL(audio.src)
}
}
audio.onerror = err => {
console.error('Audio playback error:', err)
setPlayingAudio(null)
if (audio && audio.src?.startsWith('blob:')) {
URL.revokeObjectURL(audio.src)
}
}
setAudioInstances(prev => new Map(prev).set(interaction.id, audio!))
} catch (error) {
console.error('Failed to create audio instance:', error)
setPlayingAudio(null)
return
}
}
try {
await audio.play()
} catch (playError) {
console.error('Failed to start audio playback:', playError)
setPlayingAudio(null)
}
} catch (error) {
console.error('Failed to play/stop audio:', error)
setPlayingAudio(null)
}
}
const groupedInteractions = groupInteractionsByDate(interactions)
const copyToClipboard = async (text: string, interactionId: string) => {
try {
await navigator.clipboard.writeText(text)
setCopiedItems(prev => new Set(prev).add(interactionId))
setOpenTooltipKey(`copy:${interactionId}`) // Keep tooltip open
// Reset the copied state after 2 seconds
setTimeout(() => {
setCopiedItems(prev => {
const newSet = new Set(prev)
newSet.delete(interactionId)
return newSet
})
// Close tooltip if it's still open for this item (do not override if user hovered elsewhere)
setOpenTooltipKey(prev =>
prev === `copy:${interactionId}` ? null : prev,
)
}, 2000)
} catch (error) {
console.error('Failed to copy text:', error)
}
}
const handleAudioDownload = async (interaction: Interaction) => {
try {
if (!interaction.raw_audio) {
console.warn('No audio data available for download')
return
}
const pcmData = new Uint8Array(interaction.raw_audio)
// Convert raw PCM to WAV format
const wavBuffer = createStereo48kWavFromMonoPCM(
pcmData,
interaction.sample_rate || 16000,
48000,
)
const audioBlob = new Blob([wavBuffer], { type: 'audio/wav' })
const audioUrl = URL.createObjectURL(audioBlob)
// Format filename with timestamp (YYYYMMDD_HHMMSS)
const date = new Date(interaction.created_at)
const timestamp = date
.toISOString()
.replace(/[-:]/g, '')
.replace('T', '_')
.slice(0, 15)
const filename = `ito-recording-${timestamp}.wav`
// Create temporary link and trigger download
const link = document.createElement('a')
link.href = audioUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// Clean up the blob URL
URL.revokeObjectURL(audioUrl)
} catch (error) {
console.error('Failed to download audio:', error)
}
}
return (
{/* Fixed Header Content */}
Welcome back{firstName ? `, ${firstName}!` : '!'}
}
/>
}
/>
}
/>
{/* Dictation Info Box */}
Voice dictation in any app
Hold down the trigger key
{keyboardShortcut.map((key, index) => (
{getKeyDisplay(key as KeyName, platform, {
showDirectionalText: false,
format: 'label',
})}
{index < keyboardShortcut.length - 1 && ' + '}
))}
and speak into any textbox
{/* Recent Activity Header */}
Recent activity
{/* Scrollable Recent Activity Section */}
{loading ? (
Loading recent activity...
) : interactions.length === 0 ? (
No interactions yet
Try using voice dictation by pressing{' '}
{keyboardShortcut.join(' + ')}
{/* Outer orbit circle with horizontal gradient fade (top/bottom faded) */}
{/* Inner orbit circle with horizontal gradient fade (top/bottom faded) */}
{/* Center Ito icon circle (provided SVG, horizontal gradient fade) */}
)
}
================================================
FILE: app/components/welcome/contents/GoodToGoContent.tsx
================================================
import { Button } from '@/app/components/ui/button'
import { useOnboardingStore } from '@/app/store/useOnboardingStore'
import { CheckCircle } from '@mynaui/icons-react'
export default function GoodToGoContent() {
const { incrementOnboardingStep, decrementOnboardingStep } =
useOnboardingStore()
return (
Your hardware setup is good to go!
)
}
================================================
FILE: app/components/welcome/contents/IntroducingIntelligentModeContent.tsx
================================================
import { Button } from '@/app/components/ui/button'
import { useOnboardingStore } from '@/app/store/useOnboardingStore'
import { CheckCircle } from '@mynaui/icons-react'
import { ArrowRight } from 'lucide-react'
import KeyboardShortcutEditor from '../../ui/keyboard-shortcut-editor'
import { ItoMode } from '@/app/generated/ito_pb'
import { Tip } from '../../ui/tip'
import { useSettingsStore } from '@/app/store/useSettingsStore'
export default function IntroducingIntelligentMode() {
const { incrementOnboardingStep, decrementOnboardingStep } =
useOnboardingStore()
const { getItoModeShortcuts, updateKeyboardShortcut } = useSettingsStore()
const keyboardShortcut = getItoModeShortcuts(ItoMode.EDIT)[0]
return (
Introducing Ito Intelligent Mode
What you ask gets written.
{[
'Press Hotkey -> Speak to Ito',
'Ito send your speech to LLM',
'Pastes LLM output into text box',
].map((step, index) => (
{step}
))}
Examples
{[
"Write an email to Jeff confirming tomorrow's meeting",
'Write a detailed prompt to create a picture of a tall New York building',
'Write a detailed prompt to create a stunning landing page for a dictation app',
].map((step, index) => (
{step}
))}
)
}
================================================
FILE: app/components/welcome/contents/KeyboardTestContext.tsx
================================================
import { useOnboardingStore } from '@/app/store/useOnboardingStore'
import { useSettingsStore } from '@/app/store/useSettingsStore'
import KeyboardShortcutEditor from '../../ui/keyboard-shortcut-editor'
import { ItoMode } from '@/app/generated/ito_pb'
import { getItoModeShortcutDefaults } from '@/lib/constants/keyboard-defaults'
import { usePlatform } from '@/app/hooks/usePlatform'
import { getKeyDisplay } from '@/app/utils/keyboard'
import { KeyName } from '@/lib/types/keyboard'
import React from 'react'
export default function KeyboardTestContent() {
const { incrementOnboardingStep, decrementOnboardingStep } =
useOnboardingStore()
const { getItoModeShortcuts, updateKeyboardShortcut } = useSettingsStore()
const keyboardShortcut = getItoModeShortcuts(ItoMode.TRANSCRIBE)[0]
const platform = usePlatform()
const defaultKeys = getItoModeShortcutDefaults(platform)[ItoMode.TRANSCRIBE]
return (
Press the keyboard shortcut to test it out.
We recommend the
{defaultKeys.map((key, index) => (
{getKeyDisplay(key as KeyName, platform, {
showDirectionalText: false,
format: 'label',
})}
{index < defaultKeys.length - 1 && ' + '}
))}
{' '}
key at the bottom left of the keyboard
)
}
export default function MicrophoneTestContent() {
const { incrementOnboardingStep, decrementOnboardingStep } =
useOnboardingStore()
const { microphoneDeviceId, microphoneName, setMicrophoneDeviceId } =
useSettingsStore()
const [volume, setVolume] = useState(0)
const [smoothedVolume, setSmoothedVolume] = useState(0)
// This effect listens for volume updates from the main process
useEffect(() => {
const unsubscribe = window.api.on('volume-update', (newVolume: number) => {
setVolume(newVolume)
})
// Cleanup the listener when the component unmounts
return () => {
unsubscribe()
}
}, []) // Runs only once on mount
// This effect manages the "test" recording lifecycle.
// It starts recording when a device is selected and stops when the component unmounts.
useEffect(() => {
if (microphoneDeviceId) {
console.log(`Starting test recording on device: ${microphoneDeviceId}`)
window.api.send('start-native-recording-test')
}
// Cleanup function: stop recording when the component unmounts or device changes
return () => {
console.log('Stopping test recording.')
// Use the test-specific stop handler that only stops audio recording
window.api.send('stop-native-recording-test')
}
}, [microphoneDeviceId]) // Re-runs whenever the selected microphone changes
// Smooth the volume updates to reduce flicker
useEffect(() => {
const smoothing = 0.4 // Lower = smoother, higher = more responsive
setSmoothedVolume(prev => prev * (1 - smoothing) + volume * smoothing)
}, [volume])
// Handles changing the microphone
const handleMicrophoneChange = async (deviceId: string, name: string) => {
// The useEffect hook above will automatically handle stopping the old
// stream and starting the new one when the deviceId changes.
setMicrophoneDeviceId(deviceId, name)
}
return (
Speak to test your microphone.
Your computer's built-in mic will ensure accurate transcription
with minimal latency.
)
}
// Helper function to render the appropriate auth button based on stored provider
const renderAuthButton = () => {
// If provider is email, render the inline email/password form
if (userProvider === 'email') {
return renderSingleProviderOption('email')
}
if (!userProvider || !AUTH_PROVIDERS[userProvider]) {
return renderAllAuthOptions()
}
return renderSingleProviderOption(userProvider)
}
return (
{/* Left side - Sign in form */}
{/* Logo */}
{/* Title and subtitle */}
Welcome back!
{userEmail && userProvider
? `You last logged in with ${formatProviderName(userProvider)} (${userEmail})`
: userEmail
? `You last logged in with ${userEmail}`
: 'Sign in to continue with your smart dictation.'}
{/* Auth buttons - conditionally rendered based on previous provider */}
{renderAuthButton()}
{/* Terms and privacy - only show for self-hosted */}
{(userProvider === 'self-hosted' || !userProvider) && (
Running Ito locally requires additional setup. Please refer to our{' '}
Github
{' '}
and{' '}
Documentation
)}
{/* Link to create new account */}
{userProvider
? 'Sign in with a different account?'
: 'Need to create an account?'}{' '}
{/* Right side - Placeholder for image */}
)
}
================================================
FILE: app/components/welcome/contents/TryItOutContent.tsx
================================================
import { Button } from '@/app/components/ui/button'
import { useOnboardingStore } from '@/app/store/useOnboardingStore'
import { useSettingsStore } from '@/app/store/useSettingsStore'
import SlackIcon from '../../icons/SlackIcon'
import GmailIcon from '../../icons/GmailIcon'
import ChatGPTIcon from '../../icons/ChatGPTIcon'
import NotionIcon from '../../icons/NotionIcon'
import CursorIcon from '../../icons/CursorIcon'
import { useState } from 'react'
import { ArrowUp } from '@mynaui/icons-react'
import React from 'react'
import { ItoMode } from '@/app/generated/ito_pb'
import { getKeyDisplay } from '@/app/utils/keyboard'
import { usePlatform } from '@/app/hooks/usePlatform'
import { KeyName } from '@/lib/types/keyboard'
export default function TryItOut() {
const { decrementOnboardingStep, setOnboardingCompleted } =
useOnboardingStore()
const { getItoModeShortcuts } = useSettingsStore()
const keyboardShortcut = getItoModeShortcuts(ItoMode.TRANSCRIBE)[0].keys
const platform = usePlatform()
const [selectedApp, setSelectedApp] = useState<
'slack' | 'gmail' | 'cursor' | 'chatgpt' | 'notion'
>('slack')
function renderDemo() {
if (selectedApp === 'slack') {
return (
}
export const useWindowContext = () => {
const context = useContext(WindowContext)
if (context === undefined) {
throw new Error(
'useWindowContext must be used within a WindowContextProvider',
)
}
return context
}
interface WindowContextProps {
titlebar: TitlebarProps
readonly window: WindowInitProps
}
interface WindowInitProps {
width: number
height: number
maximizable: boolean
minimizable: boolean
platform: string
}
interface WindowContextProviderProps {
children: React.ReactNode
titlebar?: TitlebarProps
}
================================================
FILE: app/generated/buf/validate/validate_pb.ts
================================================
// Copyright 2023-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.7.0 with parameter "target=ts,import_extension=.js"
// @generated from file buf/validate/validate.proto (package buf.validate, syntax proto2)
/* eslint-disable */
import type { GenEnum, GenExtension, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, extDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
import type { Duration, FieldDescriptorProto_Type, FieldOptions, MessageOptions, OneofOptions, Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_descriptor, file_google_protobuf_duration, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file buf/validate/validate.proto.
*/
export const file_buf_validate_validate: GenFile = /*@__PURE__*/
fileDesc("ChtidWYvdmFsaWRhdGUvdmFsaWRhdGUucHJvdG8SDGJ1Zi52YWxpZGF0ZSI3CgRSdWxlEgoKAmlkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSEgoKZXhwcmVzc2lvbhgDIAEoCSJuCgxNZXNzYWdlUnVsZXMSHwoDY2VsGAMgAygLMhIuYnVmLnZhbGlkYXRlLlJ1bGUSLQoFb25lb2YYBCADKAsyHi5idWYudmFsaWRhdGUuTWVzc2FnZU9uZW9mUnVsZUoECAEQAlIIZGlzYWJsZWQiNAoQTWVzc2FnZU9uZW9mUnVsZRIOCgZmaWVsZHMYASADKAkSEAoIcmVxdWlyZWQYAiABKAgiHgoKT25lb2ZSdWxlcxIQCghyZXF1aXJlZBgBIAEoCCK/CAoKRmllbGRSdWxlcxIfCgNjZWwYFyADKAsyEi5idWYudmFsaWRhdGUuUnVsZRIQCghyZXF1aXJlZBgZIAEoCBIkCgZpZ25vcmUYGyABKA4yFC5idWYudmFsaWRhdGUuSWdub3JlEikKBWZsb2F0GAEgASgLMhguYnVmLnZhbGlkYXRlLkZsb2F0UnVsZXNIABIrCgZkb3VibGUYAiABKAsyGS5idWYudmFsaWRhdGUuRG91YmxlUnVsZXNIABIpCgVpbnQzMhgDIAEoCzIYLmJ1Zi52YWxpZGF0ZS5JbnQzMlJ1bGVzSAASKQoFaW50NjQYBCABKAsyGC5idWYudmFsaWRhdGUuSW50NjRSdWxlc0gAEisKBnVpbnQzMhgFIAEoCzIZLmJ1Zi52YWxpZGF0ZS5VSW50MzJSdWxlc0gAEisKBnVpbnQ2NBgGIAEoCzIZLmJ1Zi52YWxpZGF0ZS5VSW50NjRSdWxlc0gAEisKBnNpbnQzMhgHIAEoCzIZLmJ1Zi52YWxpZGF0ZS5TSW50MzJSdWxlc0gAEisKBnNpbnQ2NBgIIAEoCzIZLmJ1Zi52YWxpZGF0ZS5TSW50NjRSdWxlc0gAEi0KB2ZpeGVkMzIYCSABKAsyGi5idWYudmFsaWRhdGUuRml4ZWQzMlJ1bGVzSAASLQoHZml4ZWQ2NBgKIAEoCzIaLmJ1Zi52YWxpZGF0ZS5GaXhlZDY0UnVsZXNIABIvCghzZml4ZWQzMhgLIAEoCzIbLmJ1Zi52YWxpZGF0ZS5TRml4ZWQzMlJ1bGVzSAASLwoIc2ZpeGVkNjQYDCABKAsyGy5idWYudmFsaWRhdGUuU0ZpeGVkNjRSdWxlc0gAEicKBGJvb2wYDSABKAsyFy5idWYudmFsaWRhdGUuQm9vbFJ1bGVzSAASKwoGc3RyaW5nGA4gASgLMhkuYnVmLnZhbGlkYXRlLlN0cmluZ1J1bGVzSAASKQoFYnl0ZXMYDyABKAsyGC5idWYudmFsaWRhdGUuQnl0ZXNSdWxlc0gAEicKBGVudW0YECABKAsyFy5idWYudmFsaWRhdGUuRW51bVJ1bGVzSAASLwoIcmVwZWF0ZWQYEiABKAsyGy5idWYudmFsaWRhdGUuUmVwZWF0ZWRSdWxlc0gAEiUKA21hcBgTIAEoCzIWLmJ1Zi52YWxpZGF0ZS5NYXBSdWxlc0gAEiUKA2FueRgUIAEoCzIWLmJ1Zi52YWxpZGF0ZS5BbnlSdWxlc0gAEi8KCGR1cmF0aW9uGBUgASgLMhsuYnVmLnZhbGlkYXRlLkR1cmF0aW9uUnVsZXNIABIxCgl0aW1lc3RhbXAYFiABKAsyHC5idWYudmFsaWRhdGUuVGltZXN0YW1wUnVsZXNIAEIGCgR0eXBlSgQIGBAZSgQIGhAbUgdza2lwcGVkUgxpZ25vcmVfZW1wdHkiVQoPUHJlZGVmaW5lZFJ1bGVzEh8KA2NlbBgBIAMoCzISLmJ1Zi52YWxpZGF0ZS5SdWxlSgQIGBAZSgQIGhAbUgdza2lwcGVkUgxpZ25vcmVfZW1wdHki2hcKCkZsb2F0UnVsZXMSgwEKBWNvbnN0GAEgASgCQnTCSHEKbwoLZmxvYXQuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKfAQoCbHQYAiABKAJCkAHCSIwBCokBCghmbG9hdC5sdBp9IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCk/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKvAQoDbHRlGAMgASgCQp8BwkibAQqYAQoJZmxvYXQubHRlGooBIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+IHJ1bGVzLmx0ZSk/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAAS7wcKAmd0GAQgASgCQuAHwkjcBwqNAQoIZmxvYXQuZ3QagAEhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwrDAQoLZmxvYXQuZ3RfbHQaswFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrNAQoVZmxvYXQuZ3RfbHRfZXhjbHVzaXZlGrMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycK0wEKDGZsb2F0Lmd0X2x0ZRrCAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCt0BChZmbG9hdC5ndF9sdGVfZXhjbHVzaXZlGsIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARK6CAoDZ3RlGAUgASgCQqoIwkimCAqbAQoJZmxvYXQuZ3RlGo0BIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCtIBCgxmbG9hdC5ndGVfbHQawQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtwBChZmbG9hdC5ndGVfbHRfZXhjbHVzaXZlGsEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwriAQoNZmxvYXQuZ3RlX2x0ZRrQAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK7AEKF2Zsb2F0Lmd0ZV9sdGVfZXhjbHVzaXZlGtABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEn8KAmluGAYgAygCQnPCSHAKbgoIZmxvYXQuaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnYKBm5vdF9pbhgHIAMoAkJmwkhjCmEKDGZsb2F0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEnUKBmZpbml0ZRgIIAEoCEJlwkhiCmAKDGZsb2F0LmZpbml0ZRpQcnVsZXMuZmluaXRlID8gKHRoaXMuaXNOYW4oKSB8fCB0aGlzLmlzSW5mKCkgPyAndmFsdWUgbXVzdCBiZSBmaW5pdGUnIDogJycpIDogJycSKwoHZXhhbXBsZRgJIAMoAkIawkgXChUKDWZsb2F0LmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIu0XCgtEb3VibGVSdWxlcxKEAQoFY29uc3QYASABKAFCdcJIcgpwCgxkb3VibGUuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKgAQoCbHQYAiABKAFCkQHCSI0BCooBCglkb3VibGUubHQafSFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQpPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASsAEKA2x0ZRgDIAEoAUKgAcJInAEKmQEKCmRvdWJsZS5sdGUaigEhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID4gcnVsZXMubHRlKT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABL0BwoCZ3QYBCABKAFC5QfCSOEHCo4BCglkb3VibGUuZ3QagAEhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwrEAQoMZG91YmxlLmd0X2x0GrMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKzgEKFmRvdWJsZS5ndF9sdF9leGNsdXNpdmUaswFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrUAQoNZG91YmxlLmd0X2x0ZRrCAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCt4BChdkb3VibGUuZ3RfbHRlX2V4Y2x1c2l2ZRrCAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAESvwgKA2d0ZRgFIAEoAUKvCMJIqwgKnAEKCmRvdWJsZS5ndGUajQEhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycK0wEKDWRvdWJsZS5ndGVfbHQawQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCt0BChdkb3VibGUuZ3RlX2x0X2V4Y2x1c2l2ZRrBAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK4wEKDmRvdWJsZS5ndGVfbHRlGtABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrtAQoYZG91YmxlLmd0ZV9sdGVfZXhjbHVzaXZlGtABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoAUJ0wkhxCm8KCWRvdWJsZS5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygBQmfCSGQKYgoNZG91YmxlLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEnYKBmZpbml0ZRgIIAEoCEJmwkhjCmEKDWRvdWJsZS5maW5pdGUaUHJ1bGVzLmZpbml0ZSA/ICh0aGlzLmlzTmFuKCkgfHwgdGhpcy5pc0luZigpID8gJ3ZhbHVlIG11c3QgYmUgZmluaXRlJyA6ICcnKSA6ICcnEiwKB2V4YW1wbGUYCSADKAFCG8JIGAoWCg5kb3VibGUuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4ijBUKCkludDMyUnVsZXMSgwEKBWNvbnN0GAEgASgFQnTCSHEKbwoLaW50MzIuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKKAQoCbHQYAiABKAVCfMJIeQp3CghpbnQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKcAQoDbHRlGAMgASgFQowBwkiIAQqFAQoJaW50MzIubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKXBwoCZ3QYBCABKAVCiAfCSIQHCnoKCGludDMyLmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwqzAQoLaW50MzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrsBChVpbnQzMi5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrDAQoMaW50MzIuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrLAQoWaW50MzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES4wcKA2d0ZRgFIAEoBULTB8JIzwcKiAEKCWludDMyLmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsIBCgxpbnQzMi5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKygEKFmludDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtIBCg1pbnQzMi5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtoBChdpbnQzMi5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ/CgJpbhgGIAMoBUJzwkhwCm4KCGludDMyLmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ2CgZub3RfaW4YByADKAVCZsJIYwphCgxpbnQzMi5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIrCgdleGFtcGxlGAggAygFQhrCSBcKFQoNaW50MzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4ijBUKCkludDY0UnVsZXMSgwEKBWNvbnN0GAEgASgDQnTCSHEKbwoLaW50NjQuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKKAQoCbHQYAiABKANCfMJIeQp3CghpbnQ2NC5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKcAQoDbHRlGAMgASgDQowBwkiIAQqFAQoJaW50NjQubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKXBwoCZ3QYBCABKANCiAfCSIQHCnoKCGludDY0Lmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwqzAQoLaW50NjQuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrsBChVpbnQ2NC5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrDAQoMaW50NjQuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrLAQoWaW50NjQuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES4wcKA2d0ZRgFIAEoA0LTB8JIzwcKiAEKCWludDY0Lmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsIBCgxpbnQ2NC5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKygEKFmludDY0Lmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtIBCg1pbnQ2NC5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtoBChdpbnQ2NC5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ/CgJpbhgGIAMoA0JzwkhwCm4KCGludDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ2CgZub3RfaW4YByADKANCZsJIYwphCgxpbnQ2NC5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIrCgdleGFtcGxlGAkgAygDQhrCSBcKFQoNaW50NjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1VJbnQzMlJ1bGVzEoQBCgVjb25zdBgBIAEoDUJ1wkhyCnAKDHVpbnQzMi5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoDUJ9wkh6CngKCXVpbnQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgNQo0BwkiJAQqGAQoKdWludDMyLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgNQo0HwkiJBwp7Cgl1aW50MzIuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgx1aW50MzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZ1aW50MzIuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXVpbnQzMi5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChd1aW50MzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoDULYB8JI1AcKiQEKCnVpbnQzMi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNdWludDMyLmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXdWludDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg51aW50MzIuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYdWludDMyLmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoDUJ0wkhxCm8KCXVpbnQzMi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygNQmfCSGQKYgoNdWludDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKA1CG8JIGAoWCg51aW50MzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1VJbnQ2NFJ1bGVzEoQBCgVjb25zdBgBIAEoBEJ1wkhyCnAKDHVpbnQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoBEJ9wkh6CngKCXVpbnQ2NC5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgEQo0BwkiJAQqGAQoKdWludDY0Lmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgEQo0HwkiJBwp7Cgl1aW50NjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgx1aW50NjQuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZ1aW50NjQuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXVpbnQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChd1aW50NjQuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoBELYB8JI1AcKiQEKCnVpbnQ2NC5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNdWludDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXdWludDY0Lmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg51aW50NjQuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYdWludDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoBEJ0wkhxCm8KCXVpbnQ2NC5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygEQmfCSGQKYgoNdWludDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKARCG8JIGAoWCg51aW50NjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1NJbnQzMlJ1bGVzEoQBCgVjb25zdBgBIAEoEUJ1wkhyCnAKDHNpbnQzMi5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoEUJ9wkh6CngKCXNpbnQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgRQo0BwkiJAQqGAQoKc2ludDMyLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgRQo0HwkiJBwp7CglzaW50MzIuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgxzaW50MzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZzaW50MzIuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXNpbnQzMi5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChdzaW50MzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoEULYB8JI1AcKiQEKCnNpbnQzMi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNc2ludDMyLmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXc2ludDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg5zaW50MzIuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYc2ludDMyLmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoEUJ0wkhxCm8KCXNpbnQzMi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygRQmfCSGQKYgoNc2ludDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKBFCG8JIGAoWCg5zaW50MzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1NJbnQ2NFJ1bGVzEoQBCgVjb25zdBgBIAEoEkJ1wkhyCnAKDHNpbnQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoEkJ9wkh6CngKCXNpbnQ2NC5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgSQo0BwkiJAQqGAQoKc2ludDY0Lmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgSQo0HwkiJBwp7CglzaW50NjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgxzaW50NjQuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZzaW50NjQuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXNpbnQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChdzaW50NjQuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoEkLYB8JI1AcKiQEKCnNpbnQ2NC5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNc2ludDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXc2ludDY0Lmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg5zaW50NjQuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYc2ludDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoEkJ0wkhxCm8KCXNpbnQ2NC5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygSQmfCSGQKYgoNc2ludDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKBJCG8JIGAoWCg5zaW50NjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4irxUKDEZpeGVkMzJSdWxlcxKFAQoFY29uc3QYASABKAdCdsJIcwpxCg1maXhlZDMyLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSjAEKAmx0GAIgASgHQn7CSHsKeQoKZml4ZWQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKeAQoDbHRlGAMgASgHQo4BwkiKAQqHAQoLZml4ZWQzMi5sdGUaeCFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEqEHCgJndBgEIAEoB0KSB8JIjgcKfAoKZml4ZWQzMi5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtQEKDWZpeGVkMzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr0BChdmaXhlZDMyLmd0X2x0X2V4Y2x1c2l2ZRqhAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCsUBCg5maXhlZDMyLmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzQEKGGZpeGVkMzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES7QcKA2d0ZRgFIAEoB0LdB8JI2QcKigEKC2ZpeGVkMzIuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKxAEKDmZpeGVkMzIuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCswBChhmaXhlZDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtQBCg9maXhlZDMyLmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK3AEKGWZpeGVkMzIuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESgQEKAmluGAYgAygHQnXCSHIKcAoKZml4ZWQzMi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSeAoGbm90X2luGAcgAygHQmjCSGUKYwoOZml4ZWQzMi5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxItCgdleGFtcGxlGAggAygHQhzCSBkKFwoPZml4ZWQzMi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKvFQoMRml4ZWQ2NFJ1bGVzEoUBCgVjb25zdBgBIAEoBkJ2wkhzCnEKDWZpeGVkNjQuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKMAQoCbHQYAiABKAZCfsJIewp5CgpmaXhlZDY0Lmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp4BCgNsdGUYAyABKAZCjgHCSIoBCocBCgtmaXhlZDY0Lmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASoQcKAmd0GAQgASgGQpIHwkiOBwp8CgpmaXhlZDY0Lmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwq1AQoNZml4ZWQ2NC5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvQEKF2ZpeGVkNjQuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxQEKDmZpeGVkNjQuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrNAQoYZml4ZWQ2NC5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLtBwoDZ3RlGAUgASgGQt0HwkjZBwqKAQoLZml4ZWQ2NC5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrEAQoOZml4ZWQ2NC5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzAEKGGZpeGVkNjQuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK1AEKD2ZpeGVkNjQuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrcAQoZZml4ZWQ2NC5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARKBAQoCaW4YBiADKAZCdcJIcgpwCgpmaXhlZDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ4CgZub3RfaW4YByADKAZCaMJIZQpjCg5maXhlZDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEi0KB2V4YW1wbGUYCCADKAZCHMJIGQoXCg9maXhlZDY0LmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIsAVCg1TRml4ZWQzMlJ1bGVzEoYBCgVjb25zdBgBIAEoD0J3wkh0CnIKDnNmaXhlZDMyLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSjQEKAmx0GAIgASgPQn/CSHwKegoLc2ZpeGVkMzIubHQaayFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID49IHJ1bGVzLmx0PyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASnwEKA2x0ZRgDIAEoD0KPAcJIiwEKiAEKDHNmaXhlZDMyLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASpgcKAmd0GAQgASgPQpcHwkiTBwp9CgtzZml4ZWQzMi5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtgEKDnNmaXhlZDMyLmd0X2x0GqMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq+AQoYc2ZpeGVkMzIuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxgEKD3NmaXhlZDMyLmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzgEKGXNmaXhlZDMyLmd0X2x0ZV9leGNsdXNpdmUasAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEvIHCgNndGUYBSABKA9C4gfCSN4HCosBCgxzZml4ZWQzMi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrFAQoPc2ZpeGVkMzIuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCs0BChlzZml4ZWQzMi5ndGVfbHRfZXhjbHVzaXZlGq8BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrVAQoQc2ZpeGVkMzIuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrdAQoac2ZpeGVkMzIuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESggEKAmluGAYgAygPQnbCSHMKcQoLc2ZpeGVkMzIuaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnkKBm5vdF9pbhgHIAMoD0JpwkhmCmQKD3NmaXhlZDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEi4KB2V4YW1wbGUYCCADKA9CHcJIGgoYChBzZml4ZWQzMi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiLAFQoNU0ZpeGVkNjRSdWxlcxKGAQoFY29uc3QYASABKBBCd8JIdApyCg5zZml4ZWQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEo0BCgJsdBgCIAEoEEJ/wkh8CnoKC3NmaXhlZDY0Lmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp8BCgNsdGUYAyABKBBCjwHCSIsBCogBCgxzZml4ZWQ2NC5sdGUaeCFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEqYHCgJndBgEIAEoEEKXB8JIkwcKfQoLc2ZpeGVkNjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrYBCg5zZml4ZWQ2NC5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvgEKGHNmaXhlZDY0Lmd0X2x0X2V4Y2x1c2l2ZRqhAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCsYBCg9zZml4ZWQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCs4BChlzZml4ZWQ2NC5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLyBwoDZ3RlGAUgASgQQuIHwkjeBwqLAQoMc2ZpeGVkNjQuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKxQEKD3NmaXhlZDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrNAQoZc2ZpeGVkNjQuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK1QEKEHNmaXhlZDY0Lmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK3QEKGnNmaXhlZDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoIBCgJpbhgGIAMoEEJ2wkhzCnEKC3NmaXhlZDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ5CgZub3RfaW4YByADKBBCacJIZgpkCg9zZml4ZWQ2NC5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIuCgdleGFtcGxlGAggAygQQh3CSBoKGAoQc2ZpeGVkNjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4ixwEKCUJvb2xSdWxlcxKCAQoFY29uc3QYASABKAhCc8JIcApuCgpib29sLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSKgoHZXhhbXBsZRgCIAMoCEIZwkgWChQKDGJvb2wuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACIpA3CgtTdHJpbmdSdWxlcxKGAQoFY29uc3QYASABKAlCd8JIdApyCgxzdHJpbmcuY29uc3QaYnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgYCVzYCcuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEn4KA2xlbhgTIAEoBEJxwkhuCmwKCnN0cmluZy5sZW4aXnVpbnQodGhpcy5zaXplKCkpICE9IHJ1bGVzLmxlbiA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSAlcyBjaGFyYWN0ZXJzJy5mb3JtYXQoW3J1bGVzLmxlbl0pIDogJycSmQEKB21pbl9sZW4YAiABKARChwHCSIMBCoABCg5zdHJpbmcubWluX2xlbhpudWludCh0aGlzLnNpemUoKSkgPCBydWxlcy5taW5fbGVuID8gJ3ZhbHVlIGxlbmd0aCBtdXN0IGJlIGF0IGxlYXN0ICVzIGNoYXJhY3RlcnMnLmZvcm1hdChbcnVsZXMubWluX2xlbl0pIDogJycSlwEKB21heF9sZW4YAyABKARChQHCSIEBCn8KDnN0cmluZy5tYXhfbGVuGm11aW50KHRoaXMuc2l6ZSgpKSA+IHJ1bGVzLm1heF9sZW4gPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgYXQgbW9zdCAlcyBjaGFyYWN0ZXJzJy5mb3JtYXQoW3J1bGVzLm1heF9sZW5dKSA6ICcnEpsBCglsZW5fYnl0ZXMYFCABKARChwHCSIMBCoABChBzdHJpbmcubGVuX2J5dGVzGmx1aW50KGJ5dGVzKHRoaXMpLnNpemUoKSkgIT0gcnVsZXMubGVuX2J5dGVzID8gJ3ZhbHVlIGxlbmd0aCBtdXN0IGJlICVzIGJ5dGVzJy5mb3JtYXQoW3J1bGVzLmxlbl9ieXRlc10pIDogJycSowEKCW1pbl9ieXRlcxgEIAEoBEKPAcJIiwEKiAEKEHN0cmluZy5taW5fYnl0ZXMadHVpbnQoYnl0ZXModGhpcykuc2l6ZSgpKSA8IHJ1bGVzLm1pbl9ieXRlcyA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSBhdCBsZWFzdCAlcyBieXRlcycuZm9ybWF0KFtydWxlcy5taW5fYnl0ZXNdKSA6ICcnEqIBCgltYXhfYnl0ZXMYBSABKARCjgHCSIoBCocBChBzdHJpbmcubWF4X2J5dGVzGnN1aW50KGJ5dGVzKHRoaXMpLnNpemUoKSkgPiBydWxlcy5tYXhfYnl0ZXMgPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgYXQgbW9zdCAlcyBieXRlcycuZm9ybWF0KFtydWxlcy5tYXhfYnl0ZXNdKSA6ICcnEo0BCgdwYXR0ZXJuGAYgASgJQnzCSHkKdwoOc3RyaW5nLnBhdHRlcm4aZSF0aGlzLm1hdGNoZXMocnVsZXMucGF0dGVybikgPyAndmFsdWUgZG9lcyBub3QgbWF0Y2ggcmVnZXggcGF0dGVybiBgJXNgJy5mb3JtYXQoW3J1bGVzLnBhdHRlcm5dKSA6ICcnEoQBCgZwcmVmaXgYByABKAlCdMJIcQpvCg1zdHJpbmcucHJlZml4Gl4hdGhpcy5zdGFydHNXaXRoKHJ1bGVzLnByZWZpeCkgPyAndmFsdWUgZG9lcyBub3QgaGF2ZSBwcmVmaXggYCVzYCcuZm9ybWF0KFtydWxlcy5wcmVmaXhdKSA6ICcnEoIBCgZzdWZmaXgYCCABKAlCcsJIbwptCg1zdHJpbmcuc3VmZml4GlwhdGhpcy5lbmRzV2l0aChydWxlcy5zdWZmaXgpID8gJ3ZhbHVlIGRvZXMgbm90IGhhdmUgc3VmZml4IGAlc2AnLmZvcm1hdChbcnVsZXMuc3VmZml4XSkgOiAnJxKQAQoIY29udGFpbnMYCSABKAlCfsJIewp5Cg9zdHJpbmcuY29udGFpbnMaZiF0aGlzLmNvbnRhaW5zKHJ1bGVzLmNvbnRhaW5zKSA/ICd2YWx1ZSBkb2VzIG5vdCBjb250YWluIHN1YnN0cmluZyBgJXNgJy5mb3JtYXQoW3J1bGVzLmNvbnRhaW5zXSkgOiAnJxKYAQoMbm90X2NvbnRhaW5zGBcgASgJQoEBwkh+CnwKE3N0cmluZy5ub3RfY29udGFpbnMaZXRoaXMuY29udGFpbnMocnVsZXMubm90X2NvbnRhaW5zKSA/ICd2YWx1ZSBjb250YWlucyBzdWJzdHJpbmcgYCVzYCcuZm9ybWF0KFtydWxlcy5ub3RfY29udGFpbnNdKSA6ICcnEoABCgJpbhgKIAMoCUJ0wkhxCm8KCXN0cmluZy5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAsgAygJQmfCSGQKYgoNc3RyaW5nLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEt8BCgVlbWFpbBgMIAEoCELNAcJIyQEKYQoMc3RyaW5nLmVtYWlsEiN2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgZW1haWwgYWRkcmVzcxosIXJ1bGVzLmVtYWlsIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0VtYWlsKCkKZAoSc3RyaW5nLmVtYWlsX2VtcHR5EjJ2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgZW1haWwgYWRkcmVzcxoaIXJ1bGVzLmVtYWlsIHx8IHRoaXMgIT0gJydIABLnAQoIaG9zdG5hbWUYDSABKAhC0gHCSM4BCmUKD3N0cmluZy5ob3N0bmFtZRIedmFsdWUgbXVzdCBiZSBhIHZhbGlkIGhvc3RuYW1lGjIhcnVsZXMuaG9zdG5hbWUgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSG9zdG5hbWUoKQplChVzdHJpbmcuaG9zdG5hbWVfZW1wdHkSLXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBob3N0bmFtZRodIXJ1bGVzLmhvc3RuYW1lIHx8IHRoaXMgIT0gJydIABLHAQoCaXAYDiABKAhCuAHCSLQBClUKCXN0cmluZy5pcBIgdmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQIGFkZHJlc3MaJiFydWxlcy5pcCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcCgpClsKD3N0cmluZy5pcF9lbXB0eRIvdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIGFkZHJlc3MaFyFydWxlcy5pcCB8fCB0aGlzICE9ICcnSAAS1gEKBGlwdjQYDyABKAhCxQHCSMEBClwKC3N0cmluZy5pcHY0EiJ2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NCBhZGRyZXNzGikhcnVsZXMuaXB2NCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcCg0KQphChFzdHJpbmcuaXB2NF9lbXB0eRIxdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjQgYWRkcmVzcxoZIXJ1bGVzLmlwdjQgfHwgdGhpcyAhPSAnJ0gAEtYBCgRpcHY2GBAgASgIQsUBwkjBAQpcCgtzdHJpbmcuaXB2NhIidmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjYgYWRkcmVzcxopIXJ1bGVzLmlwdjYgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXAoNikKYQoRc3RyaW5nLmlwdjZfZW1wdHkSMXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY2IGFkZHJlc3MaGSFydWxlcy5pcHY2IHx8IHRoaXMgIT0gJydIABK/AQoDdXJpGBEgASgIQq8BwkirAQpRCgpzdHJpbmcudXJpEhl2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgVVJJGighcnVsZXMudXJpIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc1VyaSgpClYKEHN0cmluZy51cmlfZW1wdHkSKHZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBVUkkaGCFydWxlcy51cmkgfHwgdGhpcyAhPSAnJ0gAEnAKB3VyaV9yZWYYEiABKAhCXcJIWgpYCg5zdHJpbmcudXJpX3JlZhIjdmFsdWUgbXVzdCBiZSBhIHZhbGlkIFVSSSBSZWZlcmVuY2UaISFydWxlcy51cmlfcmVmIHx8IHRoaXMuaXNVcmlSZWYoKUgAEpACCgdhZGRyZXNzGBUgASgIQvwBwkj4AQqBAQoOc3RyaW5nLmFkZHJlc3MSLXZhbHVlIG11c3QgYmUgYSB2YWxpZCBob3N0bmFtZSwgb3IgaXAgYWRkcmVzcxpAIXJ1bGVzLmFkZHJlc3MgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSG9zdG5hbWUoKSB8fCB0aGlzLmlzSXAoKQpyChRzdHJpbmcuYWRkcmVzc19lbXB0eRI8dmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIGhvc3RuYW1lLCBvciBpcCBhZGRyZXNzGhwhcnVsZXMuYWRkcmVzcyB8fCB0aGlzICE9ICcnSAASmAIKBHV1aWQYFiABKAhChwLCSIMCCqUBCgtzdHJpbmcudXVpZBIadmFsdWUgbXVzdCBiZSBhIHZhbGlkIFVVSUQaeiFydWxlcy51dWlkIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5tYXRjaGVzKCdeWzAtOWEtZkEtRl17OH0tWzAtOWEtZkEtRl17NH0tWzAtOWEtZkEtRl17NH0tWzAtOWEtZkEtRl17NH0tWzAtOWEtZkEtRl17MTJ9JCcpClkKEXN0cmluZy51dWlkX2VtcHR5Eil2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgVVVJRBoZIXJ1bGVzLnV1aWQgfHwgdGhpcyAhPSAnJ0gAEvABCgV0dXVpZBghIAEoCELeAcJI2gEKcwoMc3RyaW5nLnR1dWlkEiJ2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgdHJpbW1lZCBVVUlEGj8hcnVsZXMudHV1aWQgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLm1hdGNoZXMoJ15bMC05YS1mQS1GXXszMn0kJykKYwoSc3RyaW5nLnR1dWlkX2VtcHR5EjF2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgdHJpbW1lZCBVVUlEGhohcnVsZXMudHV1aWQgfHwgdGhpcyAhPSAnJ0gAEpYCChFpcF93aXRoX3ByZWZpeGxlbhgaIAEoCEL4AcJI9AEKeAoYc3RyaW5nLmlwX3dpdGhfcHJlZml4bGVuEh92YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVAgcHJlZml4GjshcnVsZXMuaXBfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgoKQp4Ch5zdHJpbmcuaXBfd2l0aF9wcmVmaXhsZW5fZW1wdHkSLnZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUCBwcmVmaXgaJiFydWxlcy5pcF93aXRoX3ByZWZpeGxlbiB8fCB0aGlzICE9ICcnSAASzwIKE2lwdjRfd2l0aF9wcmVmaXhsZW4YGyABKAhCrwLCSKsCCpMBChpzdHJpbmcuaXB2NF93aXRoX3ByZWZpeGxlbhI1dmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjQgYWRkcmVzcyB3aXRoIHByZWZpeCBsZW5ndGgaPiFydWxlcy5pcHY0X3dpdGhfcHJlZml4bGVuIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0lwUHJlZml4KDQpCpIBCiBzdHJpbmcuaXB2NF93aXRoX3ByZWZpeGxlbl9lbXB0eRJEdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjQgYWRkcmVzcyB3aXRoIHByZWZpeCBsZW5ndGgaKCFydWxlcy5pcHY0X3dpdGhfcHJlZml4bGVuIHx8IHRoaXMgIT0gJydIABLPAgoTaXB2Nl93aXRoX3ByZWZpeGxlbhgcIAEoCEKvAsJIqwIKkwEKGnN0cmluZy5pcHY2X3dpdGhfcHJlZml4bGVuEjV2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NiBhZGRyZXNzIHdpdGggcHJlZml4IGxlbmd0aBo+IXJ1bGVzLmlwdjZfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgoNikKkgEKIHN0cmluZy5pcHY2X3dpdGhfcHJlZml4bGVuX2VtcHR5EkR2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NiBhZGRyZXNzIHdpdGggcHJlZml4IGxlbmd0aBooIXJ1bGVzLmlwdjZfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyAhPSAnJ0gAEvIBCglpcF9wcmVmaXgYHSABKAhC3AHCSNgBCmwKEHN0cmluZy5pcF9wcmVmaXgSH3ZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUCBwcmVmaXgaNyFydWxlcy5pcF9wcmVmaXggfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgodHJ1ZSkKaAoWc3RyaW5nLmlwX3ByZWZpeF9lbXB0eRIudmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIHByZWZpeBoeIXJ1bGVzLmlwX3ByZWZpeCB8fCB0aGlzICE9ICcnSAASgwIKC2lwdjRfcHJlZml4GB4gASgIQusBwkjnAQp1ChJzdHJpbmcuaXB2NF9wcmVmaXgSIXZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUHY0IHByZWZpeBo8IXJ1bGVzLmlwdjRfcHJlZml4IHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0lwUHJlZml4KDQsIHRydWUpCm4KGHN0cmluZy5pcHY0X3ByZWZpeF9lbXB0eRIwdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjQgcHJlZml4GiAhcnVsZXMuaXB2NF9wcmVmaXggfHwgdGhpcyAhPSAnJ0gAEoMCCgtpcHY2X3ByZWZpeBgfIAEoCELrAcJI5wEKdQoSc3RyaW5nLmlwdjZfcHJlZml4EiF2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NiBwcmVmaXgaPCFydWxlcy5pcHY2X3ByZWZpeCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcFByZWZpeCg2LCB0cnVlKQpuChhzdHJpbmcuaXB2Nl9wcmVmaXhfZW1wdHkSMHZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY2IHByZWZpeBogIXJ1bGVzLmlwdjZfcHJlZml4IHx8IHRoaXMgIT0gJydIABK1AgoNaG9zdF9hbmRfcG9ydBggIAEoCEKbAsJIlwIKmQEKFHN0cmluZy5ob3N0X2FuZF9wb3J0EkF2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgaG9zdCAoaG9zdG5hbWUgb3IgSVAgYWRkcmVzcykgYW5kIHBvcnQgcGFpcho+IXJ1bGVzLmhvc3RfYW5kX3BvcnQgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSG9zdEFuZFBvcnQodHJ1ZSkKeQoac3RyaW5nLmhvc3RfYW5kX3BvcnRfZW1wdHkSN3ZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBob3N0IGFuZCBwb3J0IHBhaXIaIiFydWxlcy5ob3N0X2FuZF9wb3J0IHx8IHRoaXMgIT0gJydIABKoBQoQd2VsbF9rbm93bl9yZWdleBgYIAEoDjIYLmJ1Zi52YWxpZGF0ZS5Lbm93blJlZ2V4QvEEwkjtBArwAQojc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX25hbWUSJnZhbHVlIG11c3QgYmUgYSB2YWxpZCBIVFRQIGhlYWRlciBuYW1lGqABcnVsZXMud2VsbF9rbm93bl9yZWdleCAhPSAxIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5tYXRjaGVzKCFoYXMocnVsZXMuc3RyaWN0KSB8fCBydWxlcy5zdHJpY3QgPydeOj9bMC05YS16QS1aISMkJSZcJyorLS5eX3x+XHg2MF0rJCcgOideW15cdTAwMDBcdTAwMEFcdTAwMERdKyQnKQqNAQopc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX25hbWVfZW1wdHkSNXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBIVFRQIGhlYWRlciBuYW1lGilydWxlcy53ZWxsX2tub3duX3JlZ2V4ICE9IDEgfHwgdGhpcyAhPSAnJwrnAQokc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX3ZhbHVlEid2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSFRUUCBoZWFkZXIgdmFsdWUalQFydWxlcy53ZWxsX2tub3duX3JlZ2V4ICE9IDIgfHwgdGhpcy5tYXRjaGVzKCFoYXMocnVsZXMuc3RyaWN0KSB8fCBydWxlcy5zdHJpY3QgPydeW15cdTAwMDAtXHUwMDA4XHUwMDBBLVx1MDAxRlx1MDA3Rl0qJCcgOideW15cdTAwMDBcdTAwMEFcdTAwMERdKiQnKUgAEg4KBnN0cmljdBgZIAEoCBIsCgdleGFtcGxlGCIgAygJQhvCSBgKFgoOc3RyaW5nLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkIMCgp3ZWxsX2tub3duIuoQCgpCeXRlc1J1bGVzEoABCgVjb25zdBgBIAEoDEJxwkhuCmwKC2J5dGVzLmNvbnN0Gl10aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGJlICV4Jy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSeAoDbGVuGA0gASgEQmvCSGgKZgoJYnl0ZXMubGVuGll1aW50KHRoaXMuc2l6ZSgpKSAhPSBydWxlcy5sZW4gPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubGVuXSkgOiAnJxKQAQoHbWluX2xlbhgCIAEoBEJ/wkh8CnoKDWJ5dGVzLm1pbl9sZW4aaXVpbnQodGhpcy5zaXplKCkpIDwgcnVsZXMubWluX2xlbiA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSBhdCBsZWFzdCAlcyBieXRlcycuZm9ybWF0KFtydWxlcy5taW5fbGVuXSkgOiAnJxKIAQoHbWF4X2xlbhgDIAEoBEJ3wkh0CnIKDWJ5dGVzLm1heF9sZW4aYXVpbnQodGhpcy5zaXplKCkpID4gcnVsZXMubWF4X2xlbiA/ICd2YWx1ZSBtdXN0IGJlIGF0IG1vc3QgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubWF4X2xlbl0pIDogJycSkAEKB3BhdHRlcm4YBCABKAlCf8JIfAp6Cg1ieXRlcy5wYXR0ZXJuGmkhc3RyaW5nKHRoaXMpLm1hdGNoZXMocnVsZXMucGF0dGVybikgPyAndmFsdWUgbXVzdCBtYXRjaCByZWdleCBwYXR0ZXJuIGAlc2AnLmZvcm1hdChbcnVsZXMucGF0dGVybl0pIDogJycSgQEKBnByZWZpeBgFIAEoDEJxwkhuCmwKDGJ5dGVzLnByZWZpeBpcIXRoaXMuc3RhcnRzV2l0aChydWxlcy5wcmVmaXgpID8gJ3ZhbHVlIGRvZXMgbm90IGhhdmUgcHJlZml4ICV4Jy5mb3JtYXQoW3J1bGVzLnByZWZpeF0pIDogJycSfwoGc3VmZml4GAYgASgMQm/CSGwKagoMYnl0ZXMuc3VmZml4GlohdGhpcy5lbmRzV2l0aChydWxlcy5zdWZmaXgpID8gJ3ZhbHVlIGRvZXMgbm90IGhhdmUgc3VmZml4ICV4Jy5mb3JtYXQoW3J1bGVzLnN1ZmZpeF0pIDogJycSgwEKCGNvbnRhaW5zGAcgASgMQnHCSG4KbAoOYnl0ZXMuY29udGFpbnMaWiF0aGlzLmNvbnRhaW5zKHJ1bGVzLmNvbnRhaW5zKSA/ICd2YWx1ZSBkb2VzIG5vdCBjb250YWluICV4Jy5mb3JtYXQoW3J1bGVzLmNvbnRhaW5zXSkgOiAnJxKnAQoCaW4YCCADKAxCmgHCSJYBCpMBCghieXRlcy5pbhqGAWdldEZpZWxkKHJ1bGVzLCAnaW4nKS5zaXplKCkgPiAwICYmICEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnYKBm5vdF9pbhgJIAMoDEJmwkhjCmEKDGJ5dGVzLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEusBCgJpcBgKIAEoCELcAcJI2AEKdAoIYnl0ZXMuaXASIHZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUCBhZGRyZXNzGkYhcnVsZXMuaXAgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSA0IHx8IHRoaXMuc2l6ZSgpID09IDE2CmAKDmJ5dGVzLmlwX2VtcHR5Ei92YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVAgYWRkcmVzcxodIXJ1bGVzLmlwIHx8IHRoaXMuc2l6ZSgpICE9IDBIABLkAQoEaXB2NBgLIAEoCELTAcJIzwEKZQoKYnl0ZXMuaXB2NBIidmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjQgYWRkcmVzcxozIXJ1bGVzLmlwdjQgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSA0CmYKEGJ5dGVzLmlwdjRfZW1wdHkSMXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY0IGFkZHJlc3MaHyFydWxlcy5pcHY0IHx8IHRoaXMuc2l6ZSgpICE9IDBIABLlAQoEaXB2NhgMIAEoCELUAcJI0AEKZgoKYnl0ZXMuaXB2NhIidmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjYgYWRkcmVzcxo0IXJ1bGVzLmlwdjYgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSAxNgpmChBieXRlcy5pcHY2X2VtcHR5EjF2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NiBhZGRyZXNzGh8hcnVsZXMuaXB2NiB8fCB0aGlzLnNpemUoKSAhPSAwSAASKwoHZXhhbXBsZRgOIAMoDEIawkgXChUKDWJ5dGVzLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkIMCgp3ZWxsX2tub3duItQDCglFbnVtUnVsZXMSggEKBWNvbnN0GAEgASgFQnPCSHAKbgoKZW51bS5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEhQKDGRlZmluZWRfb25seRgCIAEoCBJ+CgJpbhgDIAMoBUJywkhvCm0KB2VudW0uaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnUKBm5vdF9pbhgEIAMoBUJlwkhiCmAKC2VudW0ubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSKgoHZXhhbXBsZRgFIAMoBUIZwkgWChQKDGVudW0uZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACIvsDCg1SZXBlYXRlZFJ1bGVzEp4BCgltaW5faXRlbXMYASABKARCigHCSIYBCoMBChJyZXBlYXRlZC5taW5faXRlbXMabXVpbnQodGhpcy5zaXplKCkpIDwgcnVsZXMubWluX2l0ZW1zID8gJ3ZhbHVlIG11c3QgY29udGFpbiBhdCBsZWFzdCAlZCBpdGVtKHMpJy5mb3JtYXQoW3J1bGVzLm1pbl9pdGVtc10pIDogJycSogEKCW1heF9pdGVtcxgCIAEoBEKOAcJIigEKhwEKEnJlcGVhdGVkLm1heF9pdGVtcxpxdWludCh0aGlzLnNpemUoKSkgPiBydWxlcy5tYXhfaXRlbXMgPyAndmFsdWUgbXVzdCBjb250YWluIG5vIG1vcmUgdGhhbiAlcyBpdGVtKHMpJy5mb3JtYXQoW3J1bGVzLm1heF9pdGVtc10pIDogJycScAoGdW5pcXVlGAMgASgIQmDCSF0KWwoPcmVwZWF0ZWQudW5pcXVlEihyZXBlYXRlZCB2YWx1ZSBtdXN0IGNvbnRhaW4gdW5pcXVlIGl0ZW1zGh4hcnVsZXMudW5pcXVlIHx8IHRoaXMudW5pcXVlKCkSJwoFaXRlbXMYBCABKAsyGC5idWYudmFsaWRhdGUuRmllbGRSdWxlcyoJCOgHEICAgIACIooDCghNYXBSdWxlcxKPAQoJbWluX3BhaXJzGAEgASgEQnzCSHkKdwoNbWFwLm1pbl9wYWlycxpmdWludCh0aGlzLnNpemUoKSkgPCBydWxlcy5taW5fcGFpcnMgPyAnbWFwIG11c3QgYmUgYXQgbGVhc3QgJWQgZW50cmllcycuZm9ybWF0KFtydWxlcy5taW5fcGFpcnNdKSA6ICcnEo4BCgltYXhfcGFpcnMYAiABKARCe8JIeAp2Cg1tYXAubWF4X3BhaXJzGmV1aW50KHRoaXMuc2l6ZSgpKSA+IHJ1bGVzLm1heF9wYWlycyA/ICdtYXAgbXVzdCBiZSBhdCBtb3N0ICVkIGVudHJpZXMnLmZvcm1hdChbcnVsZXMubWF4X3BhaXJzXSkgOiAnJxImCgRrZXlzGAQgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXMSKAoGdmFsdWVzGAUgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXMqCQjoBxCAgICAAiImCghBbnlSdWxlcxIKCgJpbhgCIAMoCRIOCgZub3RfaW4YAyADKAkimRcKDUR1cmF0aW9uUnVsZXMSoQEKBWNvbnN0GAIgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQnfCSHQKcgoOZHVyYXRpb24uY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKoAQoCbHQYAyABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25Cf8JIfAp6CgtkdXJhdGlvbi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABK6AQoDbHRlGAQgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQo8BwkiLAQqIAQoMZHVyYXRpb24ubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABLBBwoCZ3QYBSABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25ClwfCSJMHCn0KC2R1cmF0aW9uLmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwq2AQoOZHVyYXRpb24uZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr4BChhkdXJhdGlvbi5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrGAQoPZHVyYXRpb24uZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrOAQoZZHVyYXRpb24uZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAESjQgKA2d0ZRgGIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkLiB8JI3gcKiwEKDGR1cmF0aW9uLmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsUBCg9kdXJhdGlvbi5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzQEKGWR1cmF0aW9uLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtUBChBkdXJhdGlvbi5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCt0BChpkdXJhdGlvbi5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARKdAQoCaW4YByADKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CdsJIcwpxCgtkdXJhdGlvbi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSlAEKBm5vdF9pbhgIIAMoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkJpwkhmCmQKD2R1cmF0aW9uLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEkkKB2V4YW1wbGUYCSADKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CHcJIGgoYChBkdXJhdGlvbi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKSGAoOVGltZXN0YW1wUnVsZXMSowEKBWNvbnN0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEJ4wkh1CnMKD3RpbWVzdGFtcC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEqsBCgJsdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCgAHCSH0KewoMdGltZXN0YW1wLmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAErwBCgNsdGUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQpABwkiMAQqJAQoNdGltZXN0YW1wLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASbAoGbHRfbm93GAcgASgIQlrCSFcKVQoQdGltZXN0YW1wLmx0X25vdxpBKHJ1bGVzLmx0X25vdyAmJiB0aGlzID4gbm93KSA/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBub3cnIDogJydIABLHBwoCZ3QYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQpwHwkiYBwp+Cgx0aW1lc3RhbXAuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrcBCg90aW1lc3RhbXAuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr8BChl0aW1lc3RhbXAuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxwEKEHRpbWVzdGFtcC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCs8BChp0aW1lc3RhbXAuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAESkwgKA2d0ZRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBC5wfCSOMHCowBCg10aW1lc3RhbXAuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKxgEKEHRpbWVzdGFtcC5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzgEKGnRpbWVzdGFtcC5ndGVfbHRfZXhjbHVzaXZlGq8BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrWAQoRdGltZXN0YW1wLmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK3gEKG3RpbWVzdGFtcC5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJvCgZndF9ub3cYCCABKAhCXcJIWgpYChB0aW1lc3RhbXAuZ3Rfbm93GkQocnVsZXMuZ3Rfbm93ICYmIHRoaXMgPCBub3cpID8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG5vdycgOiAnJ0gBErgBCgZ3aXRoaW4YCSABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CjAHCSIgBCoUBChB0aW1lc3RhbXAud2l0aGluGnF0aGlzIDwgbm93LXJ1bGVzLndpdGhpbiB8fCB0aGlzID4gbm93K3J1bGVzLndpdGhpbiA/ICd2YWx1ZSBtdXN0IGJlIHdpdGhpbiAlcyBvZiBub3cnLmZvcm1hdChbcnVsZXMud2l0aGluXSkgOiAnJxJLCgdleGFtcGxlGAogAygLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEIewkgbChkKEXRpbWVzdGFtcC5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiI5CgpWaW9sYXRpb25zEisKCnZpb2xhdGlvbnMYASADKAsyFy5idWYudmFsaWRhdGUuVmlvbGF0aW9uIp8BCglWaW9sYXRpb24SJgoFZmllbGQYBSABKAsyFy5idWYudmFsaWRhdGUuRmllbGRQYXRoEiUKBHJ1bGUYBiABKAsyFy5idWYudmFsaWRhdGUuRmllbGRQYXRoEg8KB3J1bGVfaWQYAiABKAkSDwoHbWVzc2FnZRgDIAEoCRIPCgdmb3Jfa2V5GAQgASgISgQIARACUgpmaWVsZF9wYXRoIj0KCUZpZWxkUGF0aBIwCghlbGVtZW50cxgBIAMoCzIeLmJ1Zi52YWxpZGF0ZS5GaWVsZFBhdGhFbGVtZW50IukCChBGaWVsZFBhdGhFbGVtZW50EhQKDGZpZWxkX251bWJlchgBIAEoBRISCgpmaWVsZF9uYW1lGAIgASgJEj4KCmZpZWxkX3R5cGUYAyABKA4yKi5nb29nbGUucHJvdG9idWYuRmllbGREZXNjcmlwdG9yUHJvdG8uVHlwZRI8CghrZXlfdHlwZRgEIAEoDjIqLmdvb2dsZS5wcm90b2J1Zi5GaWVsZERlc2NyaXB0b3JQcm90by5UeXBlEj4KCnZhbHVlX3R5cGUYBSABKA4yKi5nb29nbGUucHJvdG9idWYuRmllbGREZXNjcmlwdG9yUHJvdG8uVHlwZRIPCgVpbmRleBgGIAEoBEgAEhIKCGJvb2xfa2V5GAcgASgISAASEQoHaW50X2tleRgIIAEoA0gAEhIKCHVpbnRfa2V5GAkgASgESAASFAoKc3RyaW5nX2tleRgKIAEoCUgAQgsKCXN1YnNjcmlwdCqhAQoGSWdub3JlEhYKEklHTk9SRV9VTlNQRUNJRklFRBAAEhgKFElHTk9SRV9JRl9aRVJPX1ZBTFVFEAESEQoNSUdOT1JFX0FMV0FZUxADIgQIAhACKgxJR05PUkVfRU1QVFkqDklHTk9SRV9ERUZBVUxUKhdJR05PUkVfSUZfREVGQVVMVF9WQUxVRSoVSUdOT1JFX0lGX1VOUE9QVUxBVEVEKm4KCktub3duUmVnZXgSGwoXS05PV05fUkVHRVhfVU5TUEVDSUZJRUQQABIgChxLTk9XTl9SRUdFWF9IVFRQX0hFQURFUl9OQU1FEAESIQodS05PV05fUkVHRVhfSFRUUF9IRUFERVJfVkFMVUUQAjpWCgdtZXNzYWdlEh8uZ29vZ2xlLnByb3RvYnVmLk1lc3NhZ2VPcHRpb25zGIcJIAEoCzIaLmJ1Zi52YWxpZGF0ZS5NZXNzYWdlUnVsZXNSB21lc3NhZ2U6TgoFb25lb2YSHS5nb29nbGUucHJvdG9idWYuT25lb2ZPcHRpb25zGIcJIAEoCzIYLmJ1Zi52YWxpZGF0ZS5PbmVvZlJ1bGVzUgVvbmVvZjpOCgVmaWVsZBIdLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE9wdGlvbnMYhwkgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXNSBWZpZWxkOl0KCnByZWRlZmluZWQSHS5nb29nbGUucHJvdG9idWYuRmllbGRPcHRpb25zGIgJIAEoCzIdLmJ1Zi52YWxpZGF0ZS5QcmVkZWZpbmVkUnVsZXNSCnByZWRlZmluZWRCbgoSYnVpbGQuYnVmLnZhbGlkYXRlQg1WYWxpZGF0ZVByb3RvUAFaR2J1Zi5idWlsZC9nZW4vZ28vYnVmYnVpbGQvcHJvdG92YWxpZGF0ZS9wcm90b2NvbGJ1ZmZlcnMvZ28vYnVmL3ZhbGlkYXRl", [file_google_protobuf_descriptor, file_google_protobuf_duration, file_google_protobuf_timestamp]);
/**
* `Rule` represents a validation rule written in the Common Expression
* Language (CEL) syntax. Each Rule includes a unique identifier, an
* optional error message, and the CEL expression to evaluate. For more
* information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
*
* ```proto
* message Foo {
* option (buf.validate.message).cel = {
* id: "foo.bar"
* message: "bar must be greater than 0"
* expression: "this.bar > 0"
* };
* int32 bar = 1;
* }
* ```
*
* @generated from message buf.validate.Rule
*/
export type Rule = Message<"buf.validate.Rule"> & {
/**
* `id` is a string that serves as a machine-readable name for this Rule.
* It should be unique within its scope, which could be either a message or a field.
*
* @generated from field: optional string id = 1;
*/
id: string;
/**
* `message` is an optional field that provides a human-readable error message
* for this Rule when the CEL expression evaluates to false. If a
* non-empty message is provided, any strings resulting from the CEL
* expression evaluation are ignored.
*
* @generated from field: optional string message = 2;
*/
message: string;
/**
* `expression` is the actual CEL expression that will be evaluated for
* validation. This string must resolve to either a boolean or a string
* value. If the expression evaluates to false or a non-empty string, the
* validation is considered failed, and the message is rejected.
*
* @generated from field: optional string expression = 3;
*/
expression: string;
};
/**
* Describes the message buf.validate.Rule.
* Use `create(RuleSchema)` to create a new message.
*/
export const RuleSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 0);
/**
* MessageRules represents validation rules that are applied to the entire message.
* It includes disabling options and a list of Rule messages representing Common Expression Language (CEL) validation rules.
*
* @generated from message buf.validate.MessageRules
*/
export type MessageRules = Message<"buf.validate.MessageRules"> & {
/**
* `cel` is a repeated field of type Rule. Each Rule specifies a validation rule to be applied to this message.
* These rules are written in Common Expression Language (CEL) syntax. For more information,
* [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
*
*
* ```proto
* message MyMessage {
* // The field `foo` must be greater than 42.
* option (buf.validate.message).cel = {
* id: "my_message.value",
* message: "value must be greater than 42",
* expression: "this.foo > 42",
* };
* optional int32 foo = 1;
* }
* ```
*
* @generated from field: repeated buf.validate.Rule cel = 3;
*/
cel: Rule[];
/**
* `oneof` is a repeated field of type MessageOneofRule that specifies a list of fields
* of which at most one can be present. If `required` is also specified, then exactly one
* of the specified fields _must_ be present.
*
* This will enforce oneof-like constraints with a few features not provided by
* actual Protobuf oneof declarations:
* 1. Repeated and map fields are allowed in this validation. In a Protobuf oneof,
* only scalar fields are allowed.
* 2. Fields with implicit presence are allowed. In a Protobuf oneof, all member
* fields have explicit presence. This means that, for the purpose of determining
* how many fields are set, explicitly setting such a field to its zero value is
* effectively the same as not setting it at all.
* 3. This will always generate validation errors for a message unmarshalled from
* serialized data that sets more than one field. With a Protobuf oneof, when
* multiple fields are present in the serialized form, earlier values are usually
* silently ignored when unmarshalling, with only the last field being set when
* unmarshalling completes.
*
* Note that adding a field to a `oneof` will also set the IGNORE_IF_ZERO_VALUE on the fields. This means
* only the field that is set will be validated and the unset fields are not validated according to the field rules.
* This behavior can be overridden by setting `ignore` against a field.
*
* ```proto
* message MyMessage {
* // Only one of `field1` or `field2` _can_ be present in this message.
* option (buf.validate.message).oneof = { fields: ["field1", "field2"] };
* // Exactly one of `field3` or `field4` _must_ be present in this message.
* option (buf.validate.message).oneof = { fields: ["field3", "field4"], required: true };
* string field1 = 1;
* bytes field2 = 2;
* bool field3 = 3;
* int32 field4 = 4;
* }
* ```
*
* @generated from field: repeated buf.validate.MessageOneofRule oneof = 4;
*/
oneof: MessageOneofRule[];
};
/**
* Describes the message buf.validate.MessageRules.
* Use `create(MessageRulesSchema)` to create a new message.
*/
export const MessageRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 1);
/**
* @generated from message buf.validate.MessageOneofRule
*/
export type MessageOneofRule = Message<"buf.validate.MessageOneofRule"> & {
/**
* A list of field names to include in the oneof. All field names must be
* defined in the message. At least one field must be specified, and
* duplicates are not permitted.
*
* @generated from field: repeated string fields = 1;
*/
fields: string[];
/**
* If true, one of the fields specified _must_ be set.
*
* @generated from field: optional bool required = 2;
*/
required: boolean;
};
/**
* Describes the message buf.validate.MessageOneofRule.
* Use `create(MessageOneofRuleSchema)` to create a new message.
*/
export const MessageOneofRuleSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 2);
/**
* The `OneofRules` message type enables you to manage rules for
* oneof fields in your protobuf messages.
*
* @generated from message buf.validate.OneofRules
*/
export type OneofRules = Message<"buf.validate.OneofRules"> & {
/**
* If `required` is true, exactly one field of the oneof must be set. A
* validation error is returned if no fields in the oneof are set. Further rules
* should be placed on the fields themselves to ensure they are valid values,
* such as `min_len` or `gt`.
*
* ```proto
* message MyMessage {
* oneof value {
* // Either `a` or `b` must be set. If `a` is set, it must also be
* // non-empty; whereas if `b` is set, it can still be an empty string.
* option (buf.validate.oneof).required = true;
* string a = 1 [(buf.validate.field).string.min_len = 1];
* string b = 2;
* }
* }
* ```
*
* @generated from field: optional bool required = 1;
*/
required: boolean;
};
/**
* Describes the message buf.validate.OneofRules.
* Use `create(OneofRulesSchema)` to create a new message.
*/
export const OneofRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 3);
/**
* FieldRules encapsulates the rules for each type of field. Depending on
* the field, the correct set should be used to ensure proper validations.
*
* @generated from message buf.validate.FieldRules
*/
export type FieldRules = Message<"buf.validate.FieldRules"> & {
/**
* `cel` is a repeated field used to represent a textual expression
* in the Common Expression Language (CEL) syntax. For more information,
* [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
*
* ```proto
* message MyMessage {
* // The field `value` must be greater than 42.
* optional int32 value = 1 [(buf.validate.field).cel = {
* id: "my_message.value",
* message: "value must be greater than 42",
* expression: "this > 42",
* }];
* }
* ```
*
* @generated from field: repeated buf.validate.Rule cel = 23;
*/
cel: Rule[];
/**
* If `required` is true, the field must be set. A validation error is returned
* if the field is not set.
*
* ```proto
* syntax="proto3";
*
* message FieldsWithPresence {
* // Requires any string to be set, including the empty string.
* optional string link = 1 [
* (buf.validate.field).required = true
* ];
* // Requires true or false to be set.
* optional bool disabled = 2 [
* (buf.validate.field).required = true
* ];
* // Requires a message to be set, including the empty message.
* SomeMessage msg = 4 [
* (buf.validate.field).required = true
* ];
* }
* ```
*
* All fields in the example above track presence. By default, Protovalidate
* ignores rules on those fields if no value is set. `required` ensures that
* the fields are set and valid.
*
* Fields that don't track presence are always validated by Protovalidate,
* whether they are set or not. It is not necessary to add `required`. It
* can be added to indicate that the field cannot be the zero value.
*
* ```proto
* syntax="proto3";
*
* message FieldsWithoutPresence {
* // `string.email` always applies, even to an empty string.
* string link = 1 [
* (buf.validate.field).string.email = true
* ];
* // `repeated.min_items` always applies, even to an empty list.
* repeated string labels = 2 [
* (buf.validate.field).repeated.min_items = 1
* ];
* // `required`, for fields that don't track presence, indicates
* // the value of the field can't be the zero value.
* int32 zero_value_not_allowed = 3 [
* (buf.validate.field).required = true
* ];
* }
* ```
*
* To learn which fields track presence, see the
* [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat).
*
* Note: While field rules can be applied to repeated items, map keys, and map
* values, the elements are always considered to be set. Consequently,
* specifying `repeated.items.required` is redundant.
*
* @generated from field: optional bool required = 25;
*/
required: boolean;
/**
* Ignore validation rules on the field if its value matches the specified
* criteria. See the `Ignore` enum for details.
*
* ```proto
* message UpdateRequest {
* // The uri rule only applies if the field is not an empty string.
* string url = 1 [
* (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
* (buf.validate.field).string.uri = true
* ];
* }
* ```
*
* @generated from field: optional buf.validate.Ignore ignore = 27;
*/
ignore: Ignore;
/**
* @generated from oneof buf.validate.FieldRules.type
*/
type: {
/**
* Scalar Field Types
*
* @generated from field: buf.validate.FloatRules float = 1;
*/
value: FloatRules;
case: "float";
} | {
/**
* @generated from field: buf.validate.DoubleRules double = 2;
*/
value: DoubleRules;
case: "double";
} | {
/**
* @generated from field: buf.validate.Int32Rules int32 = 3;
*/
value: Int32Rules;
case: "int32";
} | {
/**
* @generated from field: buf.validate.Int64Rules int64 = 4;
*/
value: Int64Rules;
case: "int64";
} | {
/**
* @generated from field: buf.validate.UInt32Rules uint32 = 5;
*/
value: UInt32Rules;
case: "uint32";
} | {
/**
* @generated from field: buf.validate.UInt64Rules uint64 = 6;
*/
value: UInt64Rules;
case: "uint64";
} | {
/**
* @generated from field: buf.validate.SInt32Rules sint32 = 7;
*/
value: SInt32Rules;
case: "sint32";
} | {
/**
* @generated from field: buf.validate.SInt64Rules sint64 = 8;
*/
value: SInt64Rules;
case: "sint64";
} | {
/**
* @generated from field: buf.validate.Fixed32Rules fixed32 = 9;
*/
value: Fixed32Rules;
case: "fixed32";
} | {
/**
* @generated from field: buf.validate.Fixed64Rules fixed64 = 10;
*/
value: Fixed64Rules;
case: "fixed64";
} | {
/**
* @generated from field: buf.validate.SFixed32Rules sfixed32 = 11;
*/
value: SFixed32Rules;
case: "sfixed32";
} | {
/**
* @generated from field: buf.validate.SFixed64Rules sfixed64 = 12;
*/
value: SFixed64Rules;
case: "sfixed64";
} | {
/**
* @generated from field: buf.validate.BoolRules bool = 13;
*/
value: BoolRules;
case: "bool";
} | {
/**
* @generated from field: buf.validate.StringRules string = 14;
*/
value: StringRules;
case: "string";
} | {
/**
* @generated from field: buf.validate.BytesRules bytes = 15;
*/
value: BytesRules;
case: "bytes";
} | {
/**
* Complex Field Types
*
* @generated from field: buf.validate.EnumRules enum = 16;
*/
value: EnumRules;
case: "enum";
} | {
/**
* @generated from field: buf.validate.RepeatedRules repeated = 18;
*/
value: RepeatedRules;
case: "repeated";
} | {
/**
* @generated from field: buf.validate.MapRules map = 19;
*/
value: MapRules;
case: "map";
} | {
/**
* Well-Known Field Types
*
* @generated from field: buf.validate.AnyRules any = 20;
*/
value: AnyRules;
case: "any";
} | {
/**
* @generated from field: buf.validate.DurationRules duration = 21;
*/
value: DurationRules;
case: "duration";
} | {
/**
* @generated from field: buf.validate.TimestampRules timestamp = 22;
*/
value: TimestampRules;
case: "timestamp";
} | { case: undefined; value?: undefined };
};
/**
* Describes the message buf.validate.FieldRules.
* Use `create(FieldRulesSchema)` to create a new message.
*/
export const FieldRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 4);
/**
* PredefinedRules are custom rules that can be re-used with
* multiple fields.
*
* @generated from message buf.validate.PredefinedRules
*/
export type PredefinedRules = Message<"buf.validate.PredefinedRules"> & {
/**
* `cel` is a repeated field used to represent a textual expression
* in the Common Expression Language (CEL) syntax. For more information,
* [see our documentation](https://buf.build/docs/protovalidate/schemas/predefined-rules/).
*
* ```proto
* message MyMessage {
* // The field `value` must be greater than 42.
* optional int32 value = 1 [(buf.validate.predefined).cel = {
* id: "my_message.value",
* message: "value must be greater than 42",
* expression: "this > 42",
* }];
* }
* ```
*
* @generated from field: repeated buf.validate.Rule cel = 1;
*/
cel: Rule[];
};
/**
* Describes the message buf.validate.PredefinedRules.
* Use `create(PredefinedRulesSchema)` to create a new message.
*/
export const PredefinedRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 5);
/**
* FloatRules describes the rules applied to `float` values. These
* rules may also be applied to the `google.protobuf.FloatValue` Well-Known-Type.
*
* @generated from message buf.validate.FloatRules
*/
export type FloatRules = Message<"buf.validate.FloatRules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MyFloat {
* // value must equal 42.0
* float value = 1 [(buf.validate.field).float.const = 42.0];
* }
* ```
*
* @generated from field: optional float const = 1;
*/
const: number;
/**
* @generated from oneof buf.validate.FloatRules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field <
* value). If the field value is equal to or greater than the specified value,
* an error message is generated.
*
* ```proto
* message MyFloat {
* // value must be less than 10.0
* float value = 1 [(buf.validate.field).float.lt = 10.0];
* }
* ```
*
* @generated from field: float lt = 2;
*/
value: number;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MyFloat {
* // value must be less than or equal to 10.0
* float value = 1 [(buf.validate.field).float.lte = 10.0];
* }
* ```
*
* @generated from field: float lte = 3;
*/
value: number;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.FloatRules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyFloat {
* // value must be greater than 5.0 [float.gt]
* float value = 1 [(buf.validate.field).float.gt = 5.0];
*
* // value must be greater than 5 and less than 10.0 [float.gt_lt]
* float other_value = 2 [(buf.validate.field).float = { gt: 5.0, lt: 10.0 }];
*
* // value must be greater than 10 or less than 5.0 [float.gt_lt_exclusive]
* float another_value = 3 [(buf.validate.field).float = { gt: 10.0, lt: 5.0 }];
* }
* ```
*
* @generated from field: float gt = 4;
*/
value: number;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyFloat {
* // value must be greater than or equal to 5.0 [float.gte]
* float value = 1 [(buf.validate.field).float.gte = 5.0];
*
* // value must be greater than or equal to 5.0 and less than 10.0 [float.gte_lt]
* float other_value = 2 [(buf.validate.field).float = { gte: 5.0, lt: 10.0 }];
*
* // value must be greater than or equal to 10.0 or less than 5.0 [float.gte_lt_exclusive]
* float another_value = 3 [(buf.validate.field).float = { gte: 10.0, lt: 5.0 }];
* }
* ```
*
* @generated from field: float gte = 5;
*/
value: number;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message
* is generated.
*
* ```proto
* message MyFloat {
* // value must be in list [1.0, 2.0, 3.0]
* float value = 1 [(buf.validate.field).float = { in: [1.0, 2.0, 3.0] }];
* }
* ```
*
* @generated from field: repeated float in = 6;
*/
in: number[];
/**
* `in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MyFloat {
* // value must not be in list [1.0, 2.0, 3.0]
* float value = 1 [(buf.validate.field).float = { not_in: [1.0, 2.0, 3.0] }];
* }
* ```
*
* @generated from field: repeated float not_in = 7;
*/
notIn: number[];
/**
* `finite` requires the field value to be finite. If the field value is
* infinite or NaN, an error message is generated.
*
* @generated from field: optional bool finite = 8;
*/
finite: boolean;
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyFloat {
* float value = 1 [
* (buf.validate.field).float.example = 1.0,
* (buf.validate.field).float.example = inf
* ];
* }
* ```
*
* @generated from field: repeated float example = 9;
*/
example: number[];
};
/**
* Describes the message buf.validate.FloatRules.
* Use `create(FloatRulesSchema)` to create a new message.
*/
export const FloatRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 6);
/**
* DoubleRules describes the rules applied to `double` values. These
* rules may also be applied to the `google.protobuf.DoubleValue` Well-Known-Type.
*
* @generated from message buf.validate.DoubleRules
*/
export type DoubleRules = Message<"buf.validate.DoubleRules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MyDouble {
* // value must equal 42.0
* double value = 1 [(buf.validate.field).double.const = 42.0];
* }
* ```
*
* @generated from field: optional double const = 1;
*/
const: number;
/**
* @generated from oneof buf.validate.DoubleRules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field <
* value). If the field value is equal to or greater than the specified
* value, an error message is generated.
*
* ```proto
* message MyDouble {
* // value must be less than 10.0
* double value = 1 [(buf.validate.field).double.lt = 10.0];
* }
* ```
*
* @generated from field: double lt = 2;
*/
value: number;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified value
* (field <= value). If the field value is greater than the specified value,
* an error message is generated.
*
* ```proto
* message MyDouble {
* // value must be less than or equal to 10.0
* double value = 1 [(buf.validate.field).double.lte = 10.0];
* }
* ```
*
* @generated from field: double lte = 3;
*/
value: number;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.DoubleRules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or `lte`,
* the range is reversed, and the field value must be outside the specified
* range. If the field value doesn't meet the required conditions, an error
* message is generated.
*
* ```proto
* message MyDouble {
* // value must be greater than 5.0 [double.gt]
* double value = 1 [(buf.validate.field).double.gt = 5.0];
*
* // value must be greater than 5 and less than 10.0 [double.gt_lt]
* double other_value = 2 [(buf.validate.field).double = { gt: 5.0, lt: 10.0 }];
*
* // value must be greater than 10 or less than 5.0 [double.gt_lt_exclusive]
* double another_value = 3 [(buf.validate.field).double = { gt: 10.0, lt: 5.0 }];
* }
* ```
*
* @generated from field: double gt = 4;
*/
value: number;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyDouble {
* // value must be greater than or equal to 5.0 [double.gte]
* double value = 1 [(buf.validate.field).double.gte = 5.0];
*
* // value must be greater than or equal to 5.0 and less than 10.0 [double.gte_lt]
* double other_value = 2 [(buf.validate.field).double = { gte: 5.0, lt: 10.0 }];
*
* // value must be greater than or equal to 10.0 or less than 5.0 [double.gte_lt_exclusive]
* double another_value = 3 [(buf.validate.field).double = { gte: 10.0, lt: 5.0 }];
* }
* ```
*
* @generated from field: double gte = 5;
*/
value: number;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message is
* generated.
*
* ```proto
* message MyDouble {
* // value must be in list [1.0, 2.0, 3.0]
* double value = 1 [(buf.validate.field).double = { in: [1.0, 2.0, 3.0] }];
* }
* ```
*
* @generated from field: repeated double in = 6;
*/
in: number[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MyDouble {
* // value must not be in list [1.0, 2.0, 3.0]
* double value = 1 [(buf.validate.field).double = { not_in: [1.0, 2.0, 3.0] }];
* }
* ```
*
* @generated from field: repeated double not_in = 7;
*/
notIn: number[];
/**
* `finite` requires the field value to be finite. If the field value is
* infinite or NaN, an error message is generated.
*
* @generated from field: optional bool finite = 8;
*/
finite: boolean;
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyDouble {
* double value = 1 [
* (buf.validate.field).double.example = 1.0,
* (buf.validate.field).double.example = inf
* ];
* }
* ```
*
* @generated from field: repeated double example = 9;
*/
example: number[];
};
/**
* Describes the message buf.validate.DoubleRules.
* Use `create(DoubleRulesSchema)` to create a new message.
*/
export const DoubleRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 7);
/**
* Int32Rules describes the rules applied to `int32` values. These
* rules may also be applied to the `google.protobuf.Int32Value` Well-Known-Type.
*
* @generated from message buf.validate.Int32Rules
*/
export type Int32Rules = Message<"buf.validate.Int32Rules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MyInt32 {
* // value must equal 42
* int32 value = 1 [(buf.validate.field).int32.const = 42];
* }
* ```
*
* @generated from field: optional int32 const = 1;
*/
const: number;
/**
* @generated from oneof buf.validate.Int32Rules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field
* < value). If the field value is equal to or greater than the specified
* value, an error message is generated.
*
* ```proto
* message MyInt32 {
* // value must be less than 10
* int32 value = 1 [(buf.validate.field).int32.lt = 10];
* }
* ```
*
* @generated from field: int32 lt = 2;
*/
value: number;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MyInt32 {
* // value must be less than or equal to 10
* int32 value = 1 [(buf.validate.field).int32.lte = 10];
* }
* ```
*
* @generated from field: int32 lte = 3;
*/
value: number;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.Int32Rules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyInt32 {
* // value must be greater than 5 [int32.gt]
* int32 value = 1 [(buf.validate.field).int32.gt = 5];
*
* // value must be greater than 5 and less than 10 [int32.gt_lt]
* int32 other_value = 2 [(buf.validate.field).int32 = { gt: 5, lt: 10 }];
*
* // value must be greater than 10 or less than 5 [int32.gt_lt_exclusive]
* int32 another_value = 3 [(buf.validate.field).int32 = { gt: 10, lt: 5 }];
* }
* ```
*
* @generated from field: int32 gt = 4;
*/
value: number;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified value
* (exclusive). If the value of `gte` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyInt32 {
* // value must be greater than or equal to 5 [int32.gte]
* int32 value = 1 [(buf.validate.field).int32.gte = 5];
*
* // value must be greater than or equal to 5 and less than 10 [int32.gte_lt]
* int32 other_value = 2 [(buf.validate.field).int32 = { gte: 5, lt: 10 }];
*
* // value must be greater than or equal to 10 or less than 5 [int32.gte_lt_exclusive]
* int32 another_value = 3 [(buf.validate.field).int32 = { gte: 10, lt: 5 }];
* }
* ```
*
* @generated from field: int32 gte = 5;
*/
value: number;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message is
* generated.
*
* ```proto
* message MyInt32 {
* // value must be in list [1, 2, 3]
* int32 value = 1 [(buf.validate.field).int32 = { in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated int32 in = 6;
*/
in: number[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error message
* is generated.
*
* ```proto
* message MyInt32 {
* // value must not be in list [1, 2, 3]
* int32 value = 1 [(buf.validate.field).int32 = { not_in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated int32 not_in = 7;
*/
notIn: number[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyInt32 {
* int32 value = 1 [
* (buf.validate.field).int32.example = 1,
* (buf.validate.field).int32.example = -10
* ];
* }
* ```
*
* @generated from field: repeated int32 example = 8;
*/
example: number[];
};
/**
* Describes the message buf.validate.Int32Rules.
* Use `create(Int32RulesSchema)` to create a new message.
*/
export const Int32RulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 8);
/**
* Int64Rules describes the rules applied to `int64` values. These
* rules may also be applied to the `google.protobuf.Int64Value` Well-Known-Type.
*
* @generated from message buf.validate.Int64Rules
*/
export type Int64Rules = Message<"buf.validate.Int64Rules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MyInt64 {
* // value must equal 42
* int64 value = 1 [(buf.validate.field).int64.const = 42];
* }
* ```
*
* @generated from field: optional int64 const = 1;
*/
const: bigint;
/**
* @generated from oneof buf.validate.Int64Rules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field <
* value). If the field value is equal to or greater than the specified value,
* an error message is generated.
*
* ```proto
* message MyInt64 {
* // value must be less than 10
* int64 value = 1 [(buf.validate.field).int64.lt = 10];
* }
* ```
*
* @generated from field: int64 lt = 2;
*/
value: bigint;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MyInt64 {
* // value must be less than or equal to 10
* int64 value = 1 [(buf.validate.field).int64.lte = 10];
* }
* ```
*
* @generated from field: int64 lte = 3;
*/
value: bigint;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.Int64Rules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyInt64 {
* // value must be greater than 5 [int64.gt]
* int64 value = 1 [(buf.validate.field).int64.gt = 5];
*
* // value must be greater than 5 and less than 10 [int64.gt_lt]
* int64 other_value = 2 [(buf.validate.field).int64 = { gt: 5, lt: 10 }];
*
* // value must be greater than 10 or less than 5 [int64.gt_lt_exclusive]
* int64 another_value = 3 [(buf.validate.field).int64 = { gt: 10, lt: 5 }];
* }
* ```
*
* @generated from field: int64 gt = 4;
*/
value: bigint;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyInt64 {
* // value must be greater than or equal to 5 [int64.gte]
* int64 value = 1 [(buf.validate.field).int64.gte = 5];
*
* // value must be greater than or equal to 5 and less than 10 [int64.gte_lt]
* int64 other_value = 2 [(buf.validate.field).int64 = { gte: 5, lt: 10 }];
*
* // value must be greater than or equal to 10 or less than 5 [int64.gte_lt_exclusive]
* int64 another_value = 3 [(buf.validate.field).int64 = { gte: 10, lt: 5 }];
* }
* ```
*
* @generated from field: int64 gte = 5;
*/
value: bigint;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message is
* generated.
*
* ```proto
* message MyInt64 {
* // value must be in list [1, 2, 3]
* int64 value = 1 [(buf.validate.field).int64 = { in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated int64 in = 6;
*/
in: bigint[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MyInt64 {
* // value must not be in list [1, 2, 3]
* int64 value = 1 [(buf.validate.field).int64 = { not_in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated int64 not_in = 7;
*/
notIn: bigint[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyInt64 {
* int64 value = 1 [
* (buf.validate.field).int64.example = 1,
* (buf.validate.field).int64.example = -10
* ];
* }
* ```
*
* @generated from field: repeated int64 example = 9;
*/
example: bigint[];
};
/**
* Describes the message buf.validate.Int64Rules.
* Use `create(Int64RulesSchema)` to create a new message.
*/
export const Int64RulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 9);
/**
* UInt32Rules describes the rules applied to `uint32` values. These
* rules may also be applied to the `google.protobuf.UInt32Value` Well-Known-Type.
*
* @generated from message buf.validate.UInt32Rules
*/
export type UInt32Rules = Message<"buf.validate.UInt32Rules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MyUInt32 {
* // value must equal 42
* uint32 value = 1 [(buf.validate.field).uint32.const = 42];
* }
* ```
*
* @generated from field: optional uint32 const = 1;
*/
const: number;
/**
* @generated from oneof buf.validate.UInt32Rules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field <
* value). If the field value is equal to or greater than the specified value,
* an error message is generated.
*
* ```proto
* message MyUInt32 {
* // value must be less than 10
* uint32 value = 1 [(buf.validate.field).uint32.lt = 10];
* }
* ```
*
* @generated from field: uint32 lt = 2;
*/
value: number;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MyUInt32 {
* // value must be less than or equal to 10
* uint32 value = 1 [(buf.validate.field).uint32.lte = 10];
* }
* ```
*
* @generated from field: uint32 lte = 3;
*/
value: number;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.UInt32Rules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyUInt32 {
* // value must be greater than 5 [uint32.gt]
* uint32 value = 1 [(buf.validate.field).uint32.gt = 5];
*
* // value must be greater than 5 and less than 10 [uint32.gt_lt]
* uint32 other_value = 2 [(buf.validate.field).uint32 = { gt: 5, lt: 10 }];
*
* // value must be greater than 10 or less than 5 [uint32.gt_lt_exclusive]
* uint32 another_value = 3 [(buf.validate.field).uint32 = { gt: 10, lt: 5 }];
* }
* ```
*
* @generated from field: uint32 gt = 4;
*/
value: number;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyUInt32 {
* // value must be greater than or equal to 5 [uint32.gte]
* uint32 value = 1 [(buf.validate.field).uint32.gte = 5];
*
* // value must be greater than or equal to 5 and less than 10 [uint32.gte_lt]
* uint32 other_value = 2 [(buf.validate.field).uint32 = { gte: 5, lt: 10 }];
*
* // value must be greater than or equal to 10 or less than 5 [uint32.gte_lt_exclusive]
* uint32 another_value = 3 [(buf.validate.field).uint32 = { gte: 10, lt: 5 }];
* }
* ```
*
* @generated from field: uint32 gte = 5;
*/
value: number;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message is
* generated.
*
* ```proto
* message MyUInt32 {
* // value must be in list [1, 2, 3]
* uint32 value = 1 [(buf.validate.field).uint32 = { in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated uint32 in = 6;
*/
in: number[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MyUInt32 {
* // value must not be in list [1, 2, 3]
* uint32 value = 1 [(buf.validate.field).uint32 = { not_in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated uint32 not_in = 7;
*/
notIn: number[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyUInt32 {
* uint32 value = 1 [
* (buf.validate.field).uint32.example = 1,
* (buf.validate.field).uint32.example = 10
* ];
* }
* ```
*
* @generated from field: repeated uint32 example = 8;
*/
example: number[];
};
/**
* Describes the message buf.validate.UInt32Rules.
* Use `create(UInt32RulesSchema)` to create a new message.
*/
export const UInt32RulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 10);
/**
* UInt64Rules describes the rules applied to `uint64` values. These
* rules may also be applied to the `google.protobuf.UInt64Value` Well-Known-Type.
*
* @generated from message buf.validate.UInt64Rules
*/
export type UInt64Rules = Message<"buf.validate.UInt64Rules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MyUInt64 {
* // value must equal 42
* uint64 value = 1 [(buf.validate.field).uint64.const = 42];
* }
* ```
*
* @generated from field: optional uint64 const = 1;
*/
const: bigint;
/**
* @generated from oneof buf.validate.UInt64Rules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field <
* value). If the field value is equal to or greater than the specified value,
* an error message is generated.
*
* ```proto
* message MyUInt64 {
* // value must be less than 10
* uint64 value = 1 [(buf.validate.field).uint64.lt = 10];
* }
* ```
*
* @generated from field: uint64 lt = 2;
*/
value: bigint;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MyUInt64 {
* // value must be less than or equal to 10
* uint64 value = 1 [(buf.validate.field).uint64.lte = 10];
* }
* ```
*
* @generated from field: uint64 lte = 3;
*/
value: bigint;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.UInt64Rules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyUInt64 {
* // value must be greater than 5 [uint64.gt]
* uint64 value = 1 [(buf.validate.field).uint64.gt = 5];
*
* // value must be greater than 5 and less than 10 [uint64.gt_lt]
* uint64 other_value = 2 [(buf.validate.field).uint64 = { gt: 5, lt: 10 }];
*
* // value must be greater than 10 or less than 5 [uint64.gt_lt_exclusive]
* uint64 another_value = 3 [(buf.validate.field).uint64 = { gt: 10, lt: 5 }];
* }
* ```
*
* @generated from field: uint64 gt = 4;
*/
value: bigint;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyUInt64 {
* // value must be greater than or equal to 5 [uint64.gte]
* uint64 value = 1 [(buf.validate.field).uint64.gte = 5];
*
* // value must be greater than or equal to 5 and less than 10 [uint64.gte_lt]
* uint64 other_value = 2 [(buf.validate.field).uint64 = { gte: 5, lt: 10 }];
*
* // value must be greater than or equal to 10 or less than 5 [uint64.gte_lt_exclusive]
* uint64 another_value = 3 [(buf.validate.field).uint64 = { gte: 10, lt: 5 }];
* }
* ```
*
* @generated from field: uint64 gte = 5;
*/
value: bigint;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message is
* generated.
*
* ```proto
* message MyUInt64 {
* // value must be in list [1, 2, 3]
* uint64 value = 1 [(buf.validate.field).uint64 = { in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated uint64 in = 6;
*/
in: bigint[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MyUInt64 {
* // value must not be in list [1, 2, 3]
* uint64 value = 1 [(buf.validate.field).uint64 = { not_in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated uint64 not_in = 7;
*/
notIn: bigint[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyUInt64 {
* uint64 value = 1 [
* (buf.validate.field).uint64.example = 1,
* (buf.validate.field).uint64.example = -10
* ];
* }
* ```
*
* @generated from field: repeated uint64 example = 8;
*/
example: bigint[];
};
/**
* Describes the message buf.validate.UInt64Rules.
* Use `create(UInt64RulesSchema)` to create a new message.
*/
export const UInt64RulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 11);
/**
* SInt32Rules describes the rules applied to `sint32` values.
*
* @generated from message buf.validate.SInt32Rules
*/
export type SInt32Rules = Message<"buf.validate.SInt32Rules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MySInt32 {
* // value must equal 42
* sint32 value = 1 [(buf.validate.field).sint32.const = 42];
* }
* ```
*
* @generated from field: optional sint32 const = 1;
*/
const: number;
/**
* @generated from oneof buf.validate.SInt32Rules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field
* < value). If the field value is equal to or greater than the specified
* value, an error message is generated.
*
* ```proto
* message MySInt32 {
* // value must be less than 10
* sint32 value = 1 [(buf.validate.field).sint32.lt = 10];
* }
* ```
*
* @generated from field: sint32 lt = 2;
*/
value: number;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MySInt32 {
* // value must be less than or equal to 10
* sint32 value = 1 [(buf.validate.field).sint32.lte = 10];
* }
* ```
*
* @generated from field: sint32 lte = 3;
*/
value: number;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.SInt32Rules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MySInt32 {
* // value must be greater than 5 [sint32.gt]
* sint32 value = 1 [(buf.validate.field).sint32.gt = 5];
*
* // value must be greater than 5 and less than 10 [sint32.gt_lt]
* sint32 other_value = 2 [(buf.validate.field).sint32 = { gt: 5, lt: 10 }];
*
* // value must be greater than 10 or less than 5 [sint32.gt_lt_exclusive]
* sint32 another_value = 3 [(buf.validate.field).sint32 = { gt: 10, lt: 5 }];
* }
* ```
*
* @generated from field: sint32 gt = 4;
*/
value: number;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MySInt32 {
* // value must be greater than or equal to 5 [sint32.gte]
* sint32 value = 1 [(buf.validate.field).sint32.gte = 5];
*
* // value must be greater than or equal to 5 and less than 10 [sint32.gte_lt]
* sint32 other_value = 2 [(buf.validate.field).sint32 = { gte: 5, lt: 10 }];
*
* // value must be greater than or equal to 10 or less than 5 [sint32.gte_lt_exclusive]
* sint32 another_value = 3 [(buf.validate.field).sint32 = { gte: 10, lt: 5 }];
* }
* ```
*
* @generated from field: sint32 gte = 5;
*/
value: number;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message is
* generated.
*
* ```proto
* message MySInt32 {
* // value must be in list [1, 2, 3]
* sint32 value = 1 [(buf.validate.field).sint32 = { in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated sint32 in = 6;
*/
in: number[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MySInt32 {
* // value must not be in list [1, 2, 3]
* sint32 value = 1 [(buf.validate.field).sint32 = { not_in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated sint32 not_in = 7;
*/
notIn: number[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MySInt32 {
* sint32 value = 1 [
* (buf.validate.field).sint32.example = 1,
* (buf.validate.field).sint32.example = -10
* ];
* }
* ```
*
* @generated from field: repeated sint32 example = 8;
*/
example: number[];
};
/**
* Describes the message buf.validate.SInt32Rules.
* Use `create(SInt32RulesSchema)` to create a new message.
*/
export const SInt32RulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 12);
/**
* SInt64Rules describes the rules applied to `sint64` values.
*
* @generated from message buf.validate.SInt64Rules
*/
export type SInt64Rules = Message<"buf.validate.SInt64Rules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MySInt64 {
* // value must equal 42
* sint64 value = 1 [(buf.validate.field).sint64.const = 42];
* }
* ```
*
* @generated from field: optional sint64 const = 1;
*/
const: bigint;
/**
* @generated from oneof buf.validate.SInt64Rules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field
* < value). If the field value is equal to or greater than the specified
* value, an error message is generated.
*
* ```proto
* message MySInt64 {
* // value must be less than 10
* sint64 value = 1 [(buf.validate.field).sint64.lt = 10];
* }
* ```
*
* @generated from field: sint64 lt = 2;
*/
value: bigint;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MySInt64 {
* // value must be less than or equal to 10
* sint64 value = 1 [(buf.validate.field).sint64.lte = 10];
* }
* ```
*
* @generated from field: sint64 lte = 3;
*/
value: bigint;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.SInt64Rules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MySInt64 {
* // value must be greater than 5 [sint64.gt]
* sint64 value = 1 [(buf.validate.field).sint64.gt = 5];
*
* // value must be greater than 5 and less than 10 [sint64.gt_lt]
* sint64 other_value = 2 [(buf.validate.field).sint64 = { gt: 5, lt: 10 }];
*
* // value must be greater than 10 or less than 5 [sint64.gt_lt_exclusive]
* sint64 another_value = 3 [(buf.validate.field).sint64 = { gt: 10, lt: 5 }];
* }
* ```
*
* @generated from field: sint64 gt = 4;
*/
value: bigint;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MySInt64 {
* // value must be greater than or equal to 5 [sint64.gte]
* sint64 value = 1 [(buf.validate.field).sint64.gte = 5];
*
* // value must be greater than or equal to 5 and less than 10 [sint64.gte_lt]
* sint64 other_value = 2 [(buf.validate.field).sint64 = { gte: 5, lt: 10 }];
*
* // value must be greater than or equal to 10 or less than 5 [sint64.gte_lt_exclusive]
* sint64 another_value = 3 [(buf.validate.field).sint64 = { gte: 10, lt: 5 }];
* }
* ```
*
* @generated from field: sint64 gte = 5;
*/
value: bigint;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message
* is generated.
*
* ```proto
* message MySInt64 {
* // value must be in list [1, 2, 3]
* sint64 value = 1 [(buf.validate.field).sint64 = { in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated sint64 in = 6;
*/
in: bigint[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MySInt64 {
* // value must not be in list [1, 2, 3]
* sint64 value = 1 [(buf.validate.field).sint64 = { not_in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated sint64 not_in = 7;
*/
notIn: bigint[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MySInt64 {
* sint64 value = 1 [
* (buf.validate.field).sint64.example = 1,
* (buf.validate.field).sint64.example = -10
* ];
* }
* ```
*
* @generated from field: repeated sint64 example = 8;
*/
example: bigint[];
};
/**
* Describes the message buf.validate.SInt64Rules.
* Use `create(SInt64RulesSchema)` to create a new message.
*/
export const SInt64RulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 13);
/**
* Fixed32Rules describes the rules applied to `fixed32` values.
*
* @generated from message buf.validate.Fixed32Rules
*/
export type Fixed32Rules = Message<"buf.validate.Fixed32Rules"> & {
/**
* `const` requires the field value to exactly match the specified value.
* If the field value doesn't match, an error message is generated.
*
* ```proto
* message MyFixed32 {
* // value must equal 42
* fixed32 value = 1 [(buf.validate.field).fixed32.const = 42];
* }
* ```
*
* @generated from field: optional fixed32 const = 1;
*/
const: number;
/**
* @generated from oneof buf.validate.Fixed32Rules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field <
* value). If the field value is equal to or greater than the specified value,
* an error message is generated.
*
* ```proto
* message MyFixed32 {
* // value must be less than 10
* fixed32 value = 1 [(buf.validate.field).fixed32.lt = 10];
* }
* ```
*
* @generated from field: fixed32 lt = 2;
*/
value: number;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MyFixed32 {
* // value must be less than or equal to 10
* fixed32 value = 1 [(buf.validate.field).fixed32.lte = 10];
* }
* ```
*
* @generated from field: fixed32 lte = 3;
*/
value: number;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.Fixed32Rules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyFixed32 {
* // value must be greater than 5 [fixed32.gt]
* fixed32 value = 1 [(buf.validate.field).fixed32.gt = 5];
*
* // value must be greater than 5 and less than 10 [fixed32.gt_lt]
* fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gt: 5, lt: 10 }];
*
* // value must be greater than 10 or less than 5 [fixed32.gt_lt_exclusive]
* fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gt: 10, lt: 5 }];
* }
* ```
*
* @generated from field: fixed32 gt = 4;
*/
value: number;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyFixed32 {
* // value must be greater than or equal to 5 [fixed32.gte]
* fixed32 value = 1 [(buf.validate.field).fixed32.gte = 5];
*
* // value must be greater than or equal to 5 and less than 10 [fixed32.gte_lt]
* fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gte: 5, lt: 10 }];
*
* // value must be greater than or equal to 10 or less than 5 [fixed32.gte_lt_exclusive]
* fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gte: 10, lt: 5 }];
* }
* ```
*
* @generated from field: fixed32 gte = 5;
*/
value: number;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message
* is generated.
*
* ```proto
* message MyFixed32 {
* // value must be in list [1, 2, 3]
* fixed32 value = 1 [(buf.validate.field).fixed32 = { in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated fixed32 in = 6;
*/
in: number[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MyFixed32 {
* // value must not be in list [1, 2, 3]
* fixed32 value = 1 [(buf.validate.field).fixed32 = { not_in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated fixed32 not_in = 7;
*/
notIn: number[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyFixed32 {
* fixed32 value = 1 [
* (buf.validate.field).fixed32.example = 1,
* (buf.validate.field).fixed32.example = 2
* ];
* }
* ```
*
* @generated from field: repeated fixed32 example = 8;
*/
example: number[];
};
/**
* Describes the message buf.validate.Fixed32Rules.
* Use `create(Fixed32RulesSchema)` to create a new message.
*/
export const Fixed32RulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 14);
/**
* Fixed64Rules describes the rules applied to `fixed64` values.
*
* @generated from message buf.validate.Fixed64Rules
*/
export type Fixed64Rules = Message<"buf.validate.Fixed64Rules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MyFixed64 {
* // value must equal 42
* fixed64 value = 1 [(buf.validate.field).fixed64.const = 42];
* }
* ```
*
* @generated from field: optional fixed64 const = 1;
*/
const: bigint;
/**
* @generated from oneof buf.validate.Fixed64Rules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field <
* value). If the field value is equal to or greater than the specified value,
* an error message is generated.
*
* ```proto
* message MyFixed64 {
* // value must be less than 10
* fixed64 value = 1 [(buf.validate.field).fixed64.lt = 10];
* }
* ```
*
* @generated from field: fixed64 lt = 2;
*/
value: bigint;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MyFixed64 {
* // value must be less than or equal to 10
* fixed64 value = 1 [(buf.validate.field).fixed64.lte = 10];
* }
* ```
*
* @generated from field: fixed64 lte = 3;
*/
value: bigint;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.Fixed64Rules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyFixed64 {
* // value must be greater than 5 [fixed64.gt]
* fixed64 value = 1 [(buf.validate.field).fixed64.gt = 5];
*
* // value must be greater than 5 and less than 10 [fixed64.gt_lt]
* fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gt: 5, lt: 10 }];
*
* // value must be greater than 10 or less than 5 [fixed64.gt_lt_exclusive]
* fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gt: 10, lt: 5 }];
* }
* ```
*
* @generated from field: fixed64 gt = 4;
*/
value: bigint;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyFixed64 {
* // value must be greater than or equal to 5 [fixed64.gte]
* fixed64 value = 1 [(buf.validate.field).fixed64.gte = 5];
*
* // value must be greater than or equal to 5 and less than 10 [fixed64.gte_lt]
* fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gte: 5, lt: 10 }];
*
* // value must be greater than or equal to 10 or less than 5 [fixed64.gte_lt_exclusive]
* fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gte: 10, lt: 5 }];
* }
* ```
*
* @generated from field: fixed64 gte = 5;
*/
value: bigint;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message is
* generated.
*
* ```proto
* message MyFixed64 {
* // value must be in list [1, 2, 3]
* fixed64 value = 1 [(buf.validate.field).fixed64 = { in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated fixed64 in = 6;
*/
in: bigint[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MyFixed64 {
* // value must not be in list [1, 2, 3]
* fixed64 value = 1 [(buf.validate.field).fixed64 = { not_in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated fixed64 not_in = 7;
*/
notIn: bigint[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyFixed64 {
* fixed64 value = 1 [
* (buf.validate.field).fixed64.example = 1,
* (buf.validate.field).fixed64.example = 2
* ];
* }
* ```
*
* @generated from field: repeated fixed64 example = 8;
*/
example: bigint[];
};
/**
* Describes the message buf.validate.Fixed64Rules.
* Use `create(Fixed64RulesSchema)` to create a new message.
*/
export const Fixed64RulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 15);
/**
* SFixed32Rules describes the rules applied to `fixed32` values.
*
* @generated from message buf.validate.SFixed32Rules
*/
export type SFixed32Rules = Message<"buf.validate.SFixed32Rules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MySFixed32 {
* // value must equal 42
* sfixed32 value = 1 [(buf.validate.field).sfixed32.const = 42];
* }
* ```
*
* @generated from field: optional sfixed32 const = 1;
*/
const: number;
/**
* @generated from oneof buf.validate.SFixed32Rules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field <
* value). If the field value is equal to or greater than the specified value,
* an error message is generated.
*
* ```proto
* message MySFixed32 {
* // value must be less than 10
* sfixed32 value = 1 [(buf.validate.field).sfixed32.lt = 10];
* }
* ```
*
* @generated from field: sfixed32 lt = 2;
*/
value: number;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MySFixed32 {
* // value must be less than or equal to 10
* sfixed32 value = 1 [(buf.validate.field).sfixed32.lte = 10];
* }
* ```
*
* @generated from field: sfixed32 lte = 3;
*/
value: number;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.SFixed32Rules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MySFixed32 {
* // value must be greater than 5 [sfixed32.gt]
* sfixed32 value = 1 [(buf.validate.field).sfixed32.gt = 5];
*
* // value must be greater than 5 and less than 10 [sfixed32.gt_lt]
* sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gt: 5, lt: 10 }];
*
* // value must be greater than 10 or less than 5 [sfixed32.gt_lt_exclusive]
* sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gt: 10, lt: 5 }];
* }
* ```
*
* @generated from field: sfixed32 gt = 4;
*/
value: number;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MySFixed32 {
* // value must be greater than or equal to 5 [sfixed32.gte]
* sfixed32 value = 1 [(buf.validate.field).sfixed32.gte = 5];
*
* // value must be greater than or equal to 5 and less than 10 [sfixed32.gte_lt]
* sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gte: 5, lt: 10 }];
*
* // value must be greater than or equal to 10 or less than 5 [sfixed32.gte_lt_exclusive]
* sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gte: 10, lt: 5 }];
* }
* ```
*
* @generated from field: sfixed32 gte = 5;
*/
value: number;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message is
* generated.
*
* ```proto
* message MySFixed32 {
* // value must be in list [1, 2, 3]
* sfixed32 value = 1 [(buf.validate.field).sfixed32 = { in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated sfixed32 in = 6;
*/
in: number[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MySFixed32 {
* // value must not be in list [1, 2, 3]
* sfixed32 value = 1 [(buf.validate.field).sfixed32 = { not_in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated sfixed32 not_in = 7;
*/
notIn: number[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MySFixed32 {
* sfixed32 value = 1 [
* (buf.validate.field).sfixed32.example = 1,
* (buf.validate.field).sfixed32.example = 2
* ];
* }
* ```
*
* @generated from field: repeated sfixed32 example = 8;
*/
example: number[];
};
/**
* Describes the message buf.validate.SFixed32Rules.
* Use `create(SFixed32RulesSchema)` to create a new message.
*/
export const SFixed32RulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 16);
/**
* SFixed64Rules describes the rules applied to `fixed64` values.
*
* @generated from message buf.validate.SFixed64Rules
*/
export type SFixed64Rules = Message<"buf.validate.SFixed64Rules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MySFixed64 {
* // value must equal 42
* sfixed64 value = 1 [(buf.validate.field).sfixed64.const = 42];
* }
* ```
*
* @generated from field: optional sfixed64 const = 1;
*/
const: bigint;
/**
* @generated from oneof buf.validate.SFixed64Rules.less_than
*/
lessThan: {
/**
* `lt` requires the field value to be less than the specified value (field <
* value). If the field value is equal to or greater than the specified value,
* an error message is generated.
*
* ```proto
* message MySFixed64 {
* // value must be less than 10
* sfixed64 value = 1 [(buf.validate.field).sfixed64.lt = 10];
* }
* ```
*
* @generated from field: sfixed64 lt = 2;
*/
value: bigint;
case: "lt";
} | {
/**
* `lte` requires the field value to be less than or equal to the specified
* value (field <= value). If the field value is greater than the specified
* value, an error message is generated.
*
* ```proto
* message MySFixed64 {
* // value must be less than or equal to 10
* sfixed64 value = 1 [(buf.validate.field).sfixed64.lte = 10];
* }
* ```
*
* @generated from field: sfixed64 lte = 3;
*/
value: bigint;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.SFixed64Rules.greater_than
*/
greaterThan: {
/**
* `gt` requires the field value to be greater than the specified value
* (exclusive). If the value of `gt` is larger than a specified `lt` or
* `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MySFixed64 {
* // value must be greater than 5 [sfixed64.gt]
* sfixed64 value = 1 [(buf.validate.field).sfixed64.gt = 5];
*
* // value must be greater than 5 and less than 10 [sfixed64.gt_lt]
* sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gt: 5, lt: 10 }];
*
* // value must be greater than 10 or less than 5 [sfixed64.gt_lt_exclusive]
* sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gt: 10, lt: 5 }];
* }
* ```
*
* @generated from field: sfixed64 gt = 4;
*/
value: bigint;
case: "gt";
} | {
/**
* `gte` requires the field value to be greater than or equal to the specified
* value (exclusive). If the value of `gte` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MySFixed64 {
* // value must be greater than or equal to 5 [sfixed64.gte]
* sfixed64 value = 1 [(buf.validate.field).sfixed64.gte = 5];
*
* // value must be greater than or equal to 5 and less than 10 [sfixed64.gte_lt]
* sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gte: 5, lt: 10 }];
*
* // value must be greater than or equal to 10 or less than 5 [sfixed64.gte_lt_exclusive]
* sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gte: 10, lt: 5 }];
* }
* ```
*
* @generated from field: sfixed64 gte = 5;
*/
value: bigint;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` requires the field value to be equal to one of the specified values.
* If the field value isn't one of the specified values, an error message is
* generated.
*
* ```proto
* message MySFixed64 {
* // value must be in list [1, 2, 3]
* sfixed64 value = 1 [(buf.validate.field).sfixed64 = { in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated sfixed64 in = 6;
*/
in: bigint[];
/**
* `not_in` requires the field value to not be equal to any of the specified
* values. If the field value is one of the specified values, an error
* message is generated.
*
* ```proto
* message MySFixed64 {
* // value must not be in list [1, 2, 3]
* sfixed64 value = 1 [(buf.validate.field).sfixed64 = { not_in: [1, 2, 3] }];
* }
* ```
*
* @generated from field: repeated sfixed64 not_in = 7;
*/
notIn: bigint[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MySFixed64 {
* sfixed64 value = 1 [
* (buf.validate.field).sfixed64.example = 1,
* (buf.validate.field).sfixed64.example = 2
* ];
* }
* ```
*
* @generated from field: repeated sfixed64 example = 8;
*/
example: bigint[];
};
/**
* Describes the message buf.validate.SFixed64Rules.
* Use `create(SFixed64RulesSchema)` to create a new message.
*/
export const SFixed64RulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 17);
/**
* BoolRules describes the rules applied to `bool` values. These rules
* may also be applied to the `google.protobuf.BoolValue` Well-Known-Type.
*
* @generated from message buf.validate.BoolRules
*/
export type BoolRules = Message<"buf.validate.BoolRules"> & {
/**
* `const` requires the field value to exactly match the specified boolean value.
* If the field value doesn't match, an error message is generated.
*
* ```proto
* message MyBool {
* // value must equal true
* bool value = 1 [(buf.validate.field).bool.const = true];
* }
* ```
*
* @generated from field: optional bool const = 1;
*/
const: boolean;
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyBool {
* bool value = 1 [
* (buf.validate.field).bool.example = 1,
* (buf.validate.field).bool.example = 2
* ];
* }
* ```
*
* @generated from field: repeated bool example = 2;
*/
example: boolean[];
};
/**
* Describes the message buf.validate.BoolRules.
* Use `create(BoolRulesSchema)` to create a new message.
*/
export const BoolRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 18);
/**
* StringRules describes the rules applied to `string` values These
* rules may also be applied to the `google.protobuf.StringValue` Well-Known-Type.
*
* @generated from message buf.validate.StringRules
*/
export type StringRules = Message<"buf.validate.StringRules"> & {
/**
* `const` requires the field value to exactly match the specified value. If
* the field value doesn't match, an error message is generated.
*
* ```proto
* message MyString {
* // value must equal `hello`
* string value = 1 [(buf.validate.field).string.const = "hello"];
* }
* ```
*
* @generated from field: optional string const = 1;
*/
const: string;
/**
* `len` dictates that the field value must have the specified
* number of characters (Unicode code points), which may differ from the number
* of bytes in the string. If the field value does not meet the specified
* length, an error message will be generated.
*
* ```proto
* message MyString {
* // value length must be 5 characters
* string value = 1 [(buf.validate.field).string.len = 5];
* }
* ```
*
* @generated from field: optional uint64 len = 19;
*/
len: bigint;
/**
* `min_len` specifies that the field value must have at least the specified
* number of characters (Unicode code points), which may differ from the number
* of bytes in the string. If the field value contains fewer characters, an error
* message will be generated.
*
* ```proto
* message MyString {
* // value length must be at least 3 characters
* string value = 1 [(buf.validate.field).string.min_len = 3];
* }
* ```
*
* @generated from field: optional uint64 min_len = 2;
*/
minLen: bigint;
/**
* `max_len` specifies that the field value must have no more than the specified
* number of characters (Unicode code points), which may differ from the
* number of bytes in the string. If the field value contains more characters,
* an error message will be generated.
*
* ```proto
* message MyString {
* // value length must be at most 10 characters
* string value = 1 [(buf.validate.field).string.max_len = 10];
* }
* ```
*
* @generated from field: optional uint64 max_len = 3;
*/
maxLen: bigint;
/**
* `len_bytes` dictates that the field value must have the specified number of
* bytes. If the field value does not match the specified length in bytes,
* an error message will be generated.
*
* ```proto
* message MyString {
* // value length must be 6 bytes
* string value = 1 [(buf.validate.field).string.len_bytes = 6];
* }
* ```
*
* @generated from field: optional uint64 len_bytes = 20;
*/
lenBytes: bigint;
/**
* `min_bytes` specifies that the field value must have at least the specified
* number of bytes. If the field value contains fewer bytes, an error message
* will be generated.
*
* ```proto
* message MyString {
* // value length must be at least 4 bytes
* string value = 1 [(buf.validate.field).string.min_bytes = 4];
* }
*
* ```
*
* @generated from field: optional uint64 min_bytes = 4;
*/
minBytes: bigint;
/**
* `max_bytes` specifies that the field value must have no more than the
* specified number of bytes. If the field value contains more bytes, an
* error message will be generated.
*
* ```proto
* message MyString {
* // value length must be at most 8 bytes
* string value = 1 [(buf.validate.field).string.max_bytes = 8];
* }
* ```
*
* @generated from field: optional uint64 max_bytes = 5;
*/
maxBytes: bigint;
/**
* `pattern` specifies that the field value must match the specified
* regular expression (RE2 syntax), with the expression provided without any
* delimiters. If the field value doesn't match the regular expression, an
* error message will be generated.
*
* ```proto
* message MyString {
* // value does not match regex pattern `^[a-zA-Z]//$`
* string value = 1 [(buf.validate.field).string.pattern = "^[a-zA-Z]//$"];
* }
* ```
*
* @generated from field: optional string pattern = 6;
*/
pattern: string;
/**
* `prefix` specifies that the field value must have the
* specified substring at the beginning of the string. If the field value
* doesn't start with the specified prefix, an error message will be
* generated.
*
* ```proto
* message MyString {
* // value does not have prefix `pre`
* string value = 1 [(buf.validate.field).string.prefix = "pre"];
* }
* ```
*
* @generated from field: optional string prefix = 7;
*/
prefix: string;
/**
* `suffix` specifies that the field value must have the
* specified substring at the end of the string. If the field value doesn't
* end with the specified suffix, an error message will be generated.
*
* ```proto
* message MyString {
* // value does not have suffix `post`
* string value = 1 [(buf.validate.field).string.suffix = "post"];
* }
* ```
*
* @generated from field: optional string suffix = 8;
*/
suffix: string;
/**
* `contains` specifies that the field value must have the
* specified substring anywhere in the string. If the field value doesn't
* contain the specified substring, an error message will be generated.
*
* ```proto
* message MyString {
* // value does not contain substring `inside`.
* string value = 1 [(buf.validate.field).string.contains = "inside"];
* }
* ```
*
* @generated from field: optional string contains = 9;
*/
contains: string;
/**
* `not_contains` specifies that the field value must not have the
* specified substring anywhere in the string. If the field value contains
* the specified substring, an error message will be generated.
*
* ```proto
* message MyString {
* // value contains substring `inside`.
* string value = 1 [(buf.validate.field).string.not_contains = "inside"];
* }
* ```
*
* @generated from field: optional string not_contains = 23;
*/
notContains: string;
/**
* `in` specifies that the field value must be equal to one of the specified
* values. If the field value isn't one of the specified values, an error
* message will be generated.
*
* ```proto
* message MyString {
* // value must be in list ["apple", "banana"]
* string value = 1 [(buf.validate.field).string.in = "apple", (buf.validate.field).string.in = "banana"];
* }
* ```
*
* @generated from field: repeated string in = 10;
*/
in: string[];
/**
* `not_in` specifies that the field value cannot be equal to any
* of the specified values. If the field value is one of the specified values,
* an error message will be generated.
* ```proto
* message MyString {
* // value must not be in list ["orange", "grape"]
* string value = 1 [(buf.validate.field).string.not_in = "orange", (buf.validate.field).string.not_in = "grape"];
* }
* ```
*
* @generated from field: repeated string not_in = 11;
*/
notIn: string[];
/**
* `WellKnown` rules provide advanced rules against common string
* patterns.
*
* @generated from oneof buf.validate.StringRules.well_known
*/
wellKnown: {
/**
* `email` specifies that the field value must be a valid email address, for
* example "foo@example.com".
*
* Conforms to the definition for a valid email address from the [HTML standard](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address).
* Note that this standard willfully deviates from [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322),
* which allows many unexpected forms of email addresses and will easily match
* a typographical error.
*
* If the field value isn't a valid email address, an error message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid email address
* string value = 1 [(buf.validate.field).string.email = true];
* }
* ```
*
* @generated from field: bool email = 12;
*/
value: boolean;
case: "email";
} | {
/**
* `hostname` specifies that the field value must be a valid hostname, for
* example "foo.example.com".
*
* A valid hostname follows the rules below:
* - The name consists of one or more labels, separated by a dot (".").
* - Each label can be 1 to 63 alphanumeric characters.
* - A label can contain hyphens ("-"), but must not start or end with a hyphen.
* - The right-most label must not be digits only.
* - The name can have a trailing dot—for example, "foo.example.com.".
* - The name can be 253 characters at most, excluding the optional trailing dot.
*
* If the field value isn't a valid hostname, an error message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid hostname
* string value = 1 [(buf.validate.field).string.hostname = true];
* }
* ```
*
* @generated from field: bool hostname = 13;
*/
value: boolean;
case: "hostname";
} | {
/**
* `ip` specifies that the field value must be a valid IP (v4 or v6) address.
*
* IPv4 addresses are expected in the dotted decimal format—for example, "192.168.5.21".
* IPv6 addresses are expected in their text representation—for example, "::1",
* or "2001:0DB8:ABCD:0012::0".
*
* Both formats are well-defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).
* Zone identifiers for IPv6 addresses (for example, "fe80::a%en1") are supported.
*
* If the field value isn't a valid IP address, an error message will be
* generated.
*
* ```proto
* message MyString {
* // value must be a valid IP address
* string value = 1 [(buf.validate.field).string.ip = true];
* }
* ```
*
* @generated from field: bool ip = 14;
*/
value: boolean;
case: "ip";
} | {
/**
* `ipv4` specifies that the field value must be a valid IPv4 address—for
* example "192.168.5.21". If the field value isn't a valid IPv4 address, an
* error message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid IPv4 address
* string value = 1 [(buf.validate.field).string.ipv4 = true];
* }
* ```
*
* @generated from field: bool ipv4 = 15;
*/
value: boolean;
case: "ipv4";
} | {
/**
* `ipv6` specifies that the field value must be a valid IPv6 address—for
* example "::1", or "d7a:115c:a1e0:ab12:4843:cd96:626b:430b". If the field
* value is not a valid IPv6 address, an error message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid IPv6 address
* string value = 1 [(buf.validate.field).string.ipv6 = true];
* }
* ```
*
* @generated from field: bool ipv6 = 16;
*/
value: boolean;
case: "ipv6";
} | {
/**
* `uri` specifies that the field value must be a valid URI, for example
* "https://example.com/foo/bar?baz=quux#frag".
*
* URI is defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).
* Zone Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)).
*
* If the field value isn't a valid URI, an error message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid URI
* string value = 1 [(buf.validate.field).string.uri = true];
* }
* ```
*
* @generated from field: bool uri = 17;
*/
value: boolean;
case: "uri";
} | {
/**
* `uri_ref` specifies that the field value must be a valid URI Reference—either
* a URI such as "https://example.com/foo/bar?baz=quux#frag", or a Relative
* Reference such as "./foo/bar?query".
*
* URI, URI Reference, and Relative Reference are defined in the internet
* standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). Zone
* Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)).
*
* If the field value isn't a valid URI Reference, an error message will be
* generated.
*
* ```proto
* message MyString {
* // value must be a valid URI Reference
* string value = 1 [(buf.validate.field).string.uri_ref = true];
* }
* ```
*
* @generated from field: bool uri_ref = 18;
*/
value: boolean;
case: "uriRef";
} | {
/**
* `address` specifies that the field value must be either a valid hostname
* (for example, "example.com"), or a valid IP (v4 or v6) address (for example,
* "192.168.0.1", or "::1"). If the field value isn't a valid hostname or IP,
* an error message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid hostname, or ip address
* string value = 1 [(buf.validate.field).string.address = true];
* }
* ```
*
* @generated from field: bool address = 21;
*/
value: boolean;
case: "address";
} | {
/**
* `uuid` specifies that the field value must be a valid UUID as defined by
* [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2). If the
* field value isn't a valid UUID, an error message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid UUID
* string value = 1 [(buf.validate.field).string.uuid = true];
* }
* ```
*
* @generated from field: bool uuid = 22;
*/
value: boolean;
case: "uuid";
} | {
/**
* `tuuid` (trimmed UUID) specifies that the field value must be a valid UUID as
* defined by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2) with all dashes
* omitted. If the field value isn't a valid UUID without dashes, an error message
* will be generated.
*
* ```proto
* message MyString {
* // value must be a valid trimmed UUID
* string value = 1 [(buf.validate.field).string.tuuid = true];
* }
* ```
*
* @generated from field: bool tuuid = 33;
*/
value: boolean;
case: "tuuid";
} | {
/**
* `ip_with_prefixlen` specifies that the field value must be a valid IP
* (v4 or v6) address with prefix length—for example, "192.168.5.21/16" or
* "2001:0DB8:ABCD:0012::F1/64". If the field value isn't a valid IP with
* prefix length, an error message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid IP with prefix length
* string value = 1 [(buf.validate.field).string.ip_with_prefixlen = true];
* }
* ```
*
* @generated from field: bool ip_with_prefixlen = 26;
*/
value: boolean;
case: "ipWithPrefixlen";
} | {
/**
* `ipv4_with_prefixlen` specifies that the field value must be a valid
* IPv4 address with prefix length—for example, "192.168.5.21/16". If the
* field value isn't a valid IPv4 address with prefix length, an error
* message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid IPv4 address with prefix length
* string value = 1 [(buf.validate.field).string.ipv4_with_prefixlen = true];
* }
* ```
*
* @generated from field: bool ipv4_with_prefixlen = 27;
*/
value: boolean;
case: "ipv4WithPrefixlen";
} | {
/**
* `ipv6_with_prefixlen` specifies that the field value must be a valid
* IPv6 address with prefix length—for example, "2001:0DB8:ABCD:0012::F1/64".
* If the field value is not a valid IPv6 address with prefix length,
* an error message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid IPv6 address prefix length
* string value = 1 [(buf.validate.field).string.ipv6_with_prefixlen = true];
* }
* ```
*
* @generated from field: bool ipv6_with_prefixlen = 28;
*/
value: boolean;
case: "ipv6WithPrefixlen";
} | {
/**
* `ip_prefix` specifies that the field value must be a valid IP (v4 or v6)
* prefix—for example, "192.168.0.0/16" or "2001:0DB8:ABCD:0012::0/64".
*
* The prefix must have all zeros for the unmasked bits. For example,
* "2001:0DB8:ABCD:0012::0/64" designates the left-most 64 bits for the
* prefix, and the remaining 64 bits must be zero.
*
* If the field value isn't a valid IP prefix, an error message will be
* generated.
*
* ```proto
* message MyString {
* // value must be a valid IP prefix
* string value = 1 [(buf.validate.field).string.ip_prefix = true];
* }
* ```
*
* @generated from field: bool ip_prefix = 29;
*/
value: boolean;
case: "ipPrefix";
} | {
/**
* `ipv4_prefix` specifies that the field value must be a valid IPv4
* prefix, for example "192.168.0.0/16".
*
* The prefix must have all zeros for the unmasked bits. For example,
* "192.168.0.0/16" designates the left-most 16 bits for the prefix,
* and the remaining 16 bits must be zero.
*
* If the field value isn't a valid IPv4 prefix, an error message
* will be generated.
*
* ```proto
* message MyString {
* // value must be a valid IPv4 prefix
* string value = 1 [(buf.validate.field).string.ipv4_prefix = true];
* }
* ```
*
* @generated from field: bool ipv4_prefix = 30;
*/
value: boolean;
case: "ipv4Prefix";
} | {
/**
* `ipv6_prefix` specifies that the field value must be a valid IPv6 prefix—for
* example, "2001:0DB8:ABCD:0012::0/64".
*
* The prefix must have all zeros for the unmasked bits. For example,
* "2001:0DB8:ABCD:0012::0/64" designates the left-most 64 bits for the
* prefix, and the remaining 64 bits must be zero.
*
* If the field value is not a valid IPv6 prefix, an error message will be
* generated.
*
* ```proto
* message MyString {
* // value must be a valid IPv6 prefix
* string value = 1 [(buf.validate.field).string.ipv6_prefix = true];
* }
* ```
*
* @generated from field: bool ipv6_prefix = 31;
*/
value: boolean;
case: "ipv6Prefix";
} | {
/**
* `host_and_port` specifies that the field value must be valid host/port
* pair—for example, "example.com:8080".
*
* The host can be one of:
* - An IPv4 address in dotted decimal format—for example, "192.168.5.21".
* - An IPv6 address enclosed in square brackets—for example, "[2001:0DB8:ABCD:0012::F1]".
* - A hostname—for example, "example.com".
*
* The port is separated by a colon. It must be non-empty, with a decimal number
* in the range of 0-65535, inclusive.
*
* @generated from field: bool host_and_port = 32;
*/
value: boolean;
case: "hostAndPort";
} | {
/**
* `well_known_regex` specifies a common well-known pattern
* defined as a regex. If the field value doesn't match the well-known
* regex, an error message will be generated.
*
* ```proto
* message MyString {
* // value must be a valid HTTP header value
* string value = 1 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_VALUE];
* }
* ```
*
* #### KnownRegex
*
* `well_known_regex` contains some well-known patterns.
*
* | Name | Number | Description |
* |-------------------------------|--------|-------------------------------------------|
* | KNOWN_REGEX_UNSPECIFIED | 0 | |
* | KNOWN_REGEX_HTTP_HEADER_NAME | 1 | HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2) |
* | KNOWN_REGEX_HTTP_HEADER_VALUE | 2 | HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4) |
*
* @generated from field: buf.validate.KnownRegex well_known_regex = 24;
*/
value: KnownRegex;
case: "wellKnownRegex";
} | { case: undefined; value?: undefined };
/**
* This applies to regexes `HTTP_HEADER_NAME` and `HTTP_HEADER_VALUE` to
* enable strict header validation. By default, this is true, and HTTP header
* validations are [RFC-compliant](https://datatracker.ietf.org/doc/html/rfc7230#section-3). Setting to false will enable looser
* validations that only disallow `\r\n\0` characters, which can be used to
* bypass header matching rules.
*
* ```proto
* message MyString {
* // The field `value` must have be a valid HTTP headers, but not enforced with strict rules.
* string value = 1 [(buf.validate.field).string.strict = false];
* }
* ```
*
* @generated from field: optional bool strict = 25;
*/
strict: boolean;
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyString {
* string value = 1 [
* (buf.validate.field).string.example = "hello",
* (buf.validate.field).string.example = "world"
* ];
* }
* ```
*
* @generated from field: repeated string example = 34;
*/
example: string[];
};
/**
* Describes the message buf.validate.StringRules.
* Use `create(StringRulesSchema)` to create a new message.
*/
export const StringRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 19);
/**
* BytesRules describe the rules applied to `bytes` values. These rules
* may also be applied to the `google.protobuf.BytesValue` Well-Known-Type.
*
* @generated from message buf.validate.BytesRules
*/
export type BytesRules = Message<"buf.validate.BytesRules"> & {
/**
* `const` requires the field value to exactly match the specified bytes
* value. If the field value doesn't match, an error message is generated.
*
* ```proto
* message MyBytes {
* // value must be "\x01\x02\x03\x04"
* bytes value = 1 [(buf.validate.field).bytes.const = "\x01\x02\x03\x04"];
* }
* ```
*
* @generated from field: optional bytes const = 1;
*/
const: Uint8Array;
/**
* `len` requires the field value to have the specified length in bytes.
* If the field value doesn't match, an error message is generated.
*
* ```proto
* message MyBytes {
* // value length must be 4 bytes.
* optional bytes value = 1 [(buf.validate.field).bytes.len = 4];
* }
* ```
*
* @generated from field: optional uint64 len = 13;
*/
len: bigint;
/**
* `min_len` requires the field value to have at least the specified minimum
* length in bytes.
* If the field value doesn't meet the requirement, an error message is generated.
*
* ```proto
* message MyBytes {
* // value length must be at least 2 bytes.
* optional bytes value = 1 [(buf.validate.field).bytes.min_len = 2];
* }
* ```
*
* @generated from field: optional uint64 min_len = 2;
*/
minLen: bigint;
/**
* `max_len` requires the field value to have at most the specified maximum
* length in bytes.
* If the field value exceeds the requirement, an error message is generated.
*
* ```proto
* message MyBytes {
* // value must be at most 6 bytes.
* optional bytes value = 1 [(buf.validate.field).bytes.max_len = 6];
* }
* ```
*
* @generated from field: optional uint64 max_len = 3;
*/
maxLen: bigint;
/**
* `pattern` requires the field value to match the specified regular
* expression ([RE2 syntax](https://github.com/google/re2/wiki/Syntax)).
* The value of the field must be valid UTF-8 or validation will fail with a
* runtime error.
* If the field value doesn't match the pattern, an error message is generated.
*
* ```proto
* message MyBytes {
* // value must match regex pattern "^[a-zA-Z0-9]+$".
* optional bytes value = 1 [(buf.validate.field).bytes.pattern = "^[a-zA-Z0-9]+$"];
* }
* ```
*
* @generated from field: optional string pattern = 4;
*/
pattern: string;
/**
* `prefix` requires the field value to have the specified bytes at the
* beginning of the string.
* If the field value doesn't meet the requirement, an error message is generated.
*
* ```proto
* message MyBytes {
* // value does not have prefix \x01\x02
* optional bytes value = 1 [(buf.validate.field).bytes.prefix = "\x01\x02"];
* }
* ```
*
* @generated from field: optional bytes prefix = 5;
*/
prefix: Uint8Array;
/**
* `suffix` requires the field value to have the specified bytes at the end
* of the string.
* If the field value doesn't meet the requirement, an error message is generated.
*
* ```proto
* message MyBytes {
* // value does not have suffix \x03\x04
* optional bytes value = 1 [(buf.validate.field).bytes.suffix = "\x03\x04"];
* }
* ```
*
* @generated from field: optional bytes suffix = 6;
*/
suffix: Uint8Array;
/**
* `contains` requires the field value to have the specified bytes anywhere in
* the string.
* If the field value doesn't meet the requirement, an error message is generated.
*
* ```protobuf
* message MyBytes {
* // value does not contain \x02\x03
* optional bytes value = 1 [(buf.validate.field).bytes.contains = "\x02\x03"];
* }
* ```
*
* @generated from field: optional bytes contains = 7;
*/
contains: Uint8Array;
/**
* `in` requires the field value to be equal to one of the specified
* values. If the field value doesn't match any of the specified values, an
* error message is generated.
*
* ```protobuf
* message MyBytes {
* // value must in ["\x01\x02", "\x02\x03", "\x03\x04"]
* optional bytes value = 1 [(buf.validate.field).bytes.in = {"\x01\x02", "\x02\x03", "\x03\x04"}];
* }
* ```
*
* @generated from field: repeated bytes in = 8;
*/
in: Uint8Array[];
/**
* `not_in` requires the field value to be not equal to any of the specified
* values.
* If the field value matches any of the specified values, an error message is
* generated.
*
* ```proto
* message MyBytes {
* // value must not in ["\x01\x02", "\x02\x03", "\x03\x04"]
* optional bytes value = 1 [(buf.validate.field).bytes.not_in = {"\x01\x02", "\x02\x03", "\x03\x04"}];
* }
* ```
*
* @generated from field: repeated bytes not_in = 9;
*/
notIn: Uint8Array[];
/**
* WellKnown rules provide advanced rules against common byte
* patterns
*
* @generated from oneof buf.validate.BytesRules.well_known
*/
wellKnown: {
/**
* `ip` ensures that the field `value` is a valid IP address (v4 or v6) in byte format.
* If the field value doesn't meet this rule, an error message is generated.
*
* ```proto
* message MyBytes {
* // value must be a valid IP address
* optional bytes value = 1 [(buf.validate.field).bytes.ip = true];
* }
* ```
*
* @generated from field: bool ip = 10;
*/
value: boolean;
case: "ip";
} | {
/**
* `ipv4` ensures that the field `value` is a valid IPv4 address in byte format.
* If the field value doesn't meet this rule, an error message is generated.
*
* ```proto
* message MyBytes {
* // value must be a valid IPv4 address
* optional bytes value = 1 [(buf.validate.field).bytes.ipv4 = true];
* }
* ```
*
* @generated from field: bool ipv4 = 11;
*/
value: boolean;
case: "ipv4";
} | {
/**
* `ipv6` ensures that the field `value` is a valid IPv6 address in byte format.
* If the field value doesn't meet this rule, an error message is generated.
* ```proto
* message MyBytes {
* // value must be a valid IPv6 address
* optional bytes value = 1 [(buf.validate.field).bytes.ipv6 = true];
* }
* ```
*
* @generated from field: bool ipv6 = 12;
*/
value: boolean;
case: "ipv6";
} | { case: undefined; value?: undefined };
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyBytes {
* bytes value = 1 [
* (buf.validate.field).bytes.example = "\x01\x02",
* (buf.validate.field).bytes.example = "\x02\x03"
* ];
* }
* ```
*
* @generated from field: repeated bytes example = 14;
*/
example: Uint8Array[];
};
/**
* Describes the message buf.validate.BytesRules.
* Use `create(BytesRulesSchema)` to create a new message.
*/
export const BytesRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 20);
/**
* EnumRules describe the rules applied to `enum` values.
*
* @generated from message buf.validate.EnumRules
*/
export type EnumRules = Message<"buf.validate.EnumRules"> & {
/**
* `const` requires the field value to exactly match the specified enum value.
* If the field value doesn't match, an error message is generated.
*
* ```proto
* enum MyEnum {
* MY_ENUM_UNSPECIFIED = 0;
* MY_ENUM_VALUE1 = 1;
* MY_ENUM_VALUE2 = 2;
* }
*
* message MyMessage {
* // The field `value` must be exactly MY_ENUM_VALUE1.
* MyEnum value = 1 [(buf.validate.field).enum.const = 1];
* }
* ```
*
* @generated from field: optional int32 const = 1;
*/
const: number;
/**
* `defined_only` requires the field value to be one of the defined values for
* this enum, failing on any undefined value.
*
* ```proto
* enum MyEnum {
* MY_ENUM_UNSPECIFIED = 0;
* MY_ENUM_VALUE1 = 1;
* MY_ENUM_VALUE2 = 2;
* }
*
* message MyMessage {
* // The field `value` must be a defined value of MyEnum.
* MyEnum value = 1 [(buf.validate.field).enum.defined_only = true];
* }
* ```
*
* @generated from field: optional bool defined_only = 2;
*/
definedOnly: boolean;
/**
* `in` requires the field value to be equal to one of the
* specified enum values. If the field value doesn't match any of the
* specified values, an error message is generated.
*
* ```proto
* enum MyEnum {
* MY_ENUM_UNSPECIFIED = 0;
* MY_ENUM_VALUE1 = 1;
* MY_ENUM_VALUE2 = 2;
* }
*
* message MyMessage {
* // The field `value` must be equal to one of the specified values.
* MyEnum value = 1 [(buf.validate.field).enum = { in: [1, 2]}];
* }
* ```
*
* @generated from field: repeated int32 in = 3;
*/
in: number[];
/**
* `not_in` requires the field value to be not equal to any of the
* specified enum values. If the field value matches one of the specified
* values, an error message is generated.
*
* ```proto
* enum MyEnum {
* MY_ENUM_UNSPECIFIED = 0;
* MY_ENUM_VALUE1 = 1;
* MY_ENUM_VALUE2 = 2;
* }
*
* message MyMessage {
* // The field `value` must not be equal to any of the specified values.
* MyEnum value = 1 [(buf.validate.field).enum = { not_in: [1, 2]}];
* }
* ```
*
* @generated from field: repeated int32 not_in = 4;
*/
notIn: number[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* enum MyEnum {
* MY_ENUM_UNSPECIFIED = 0;
* MY_ENUM_VALUE1 = 1;
* MY_ENUM_VALUE2 = 2;
* }
*
* message MyMessage {
* (buf.validate.field).enum.example = 1,
* (buf.validate.field).enum.example = 2
* }
* ```
*
* @generated from field: repeated int32 example = 5;
*/
example: number[];
};
/**
* Describes the message buf.validate.EnumRules.
* Use `create(EnumRulesSchema)` to create a new message.
*/
export const EnumRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 21);
/**
* RepeatedRules describe the rules applied to `repeated` values.
*
* @generated from message buf.validate.RepeatedRules
*/
export type RepeatedRules = Message<"buf.validate.RepeatedRules"> & {
/**
* `min_items` requires that this field must contain at least the specified
* minimum number of items.
*
* Note that `min_items = 1` is equivalent to setting a field as `required`.
*
* ```proto
* message MyRepeated {
* // value must contain at least 2 items
* repeated string value = 1 [(buf.validate.field).repeated.min_items = 2];
* }
* ```
*
* @generated from field: optional uint64 min_items = 1;
*/
minItems: bigint;
/**
* `max_items` denotes that this field must not exceed a
* certain number of items as the upper limit. If the field contains more
* items than specified, an error message will be generated, requiring the
* field to maintain no more than the specified number of items.
*
* ```proto
* message MyRepeated {
* // value must contain no more than 3 item(s)
* repeated string value = 1 [(buf.validate.field).repeated.max_items = 3];
* }
* ```
*
* @generated from field: optional uint64 max_items = 2;
*/
maxItems: bigint;
/**
* `unique` indicates that all elements in this field must
* be unique. This rule is strictly applicable to scalar and enum
* types, with message types not being supported.
*
* ```proto
* message MyRepeated {
* // repeated value must contain unique items
* repeated string value = 1 [(buf.validate.field).repeated.unique = true];
* }
* ```
*
* @generated from field: optional bool unique = 3;
*/
unique: boolean;
/**
* `items` details the rules to be applied to each item
* in the field. Even for repeated message fields, validation is executed
* against each item unless `ignore` is specified.
*
* ```proto
* message MyRepeated {
* // The items in the field `value` must follow the specified rules.
* repeated string value = 1 [(buf.validate.field).repeated.items = {
* string: {
* min_len: 3
* max_len: 10
* }
* }];
* }
* ```
*
* Note that the `required` rule does not apply. Repeated items
* cannot be unset.
*
* @generated from field: optional buf.validate.FieldRules items = 4;
*/
items?: FieldRules;
};
/**
* Describes the message buf.validate.RepeatedRules.
* Use `create(RepeatedRulesSchema)` to create a new message.
*/
export const RepeatedRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 22);
/**
* MapRules describe the rules applied to `map` values.
*
* @generated from message buf.validate.MapRules
*/
export type MapRules = Message<"buf.validate.MapRules"> & {
/**
* Specifies the minimum number of key-value pairs allowed. If the field has
* fewer key-value pairs than specified, an error message is generated.
*
* ```proto
* message MyMap {
* // The field `value` must have at least 2 key-value pairs.
* map value = 1 [(buf.validate.field).map.min_pairs = 2];
* }
* ```
*
* @generated from field: optional uint64 min_pairs = 1;
*/
minPairs: bigint;
/**
* Specifies the maximum number of key-value pairs allowed. If the field has
* more key-value pairs than specified, an error message is generated.
*
* ```proto
* message MyMap {
* // The field `value` must have at most 3 key-value pairs.
* map value = 1 [(buf.validate.field).map.max_pairs = 3];
* }
* ```
*
* @generated from field: optional uint64 max_pairs = 2;
*/
maxPairs: bigint;
/**
* Specifies the rules to be applied to each key in the field.
*
* ```proto
* message MyMap {
* // The keys in the field `value` must follow the specified rules.
* map value = 1 [(buf.validate.field).map.keys = {
* string: {
* min_len: 3
* max_len: 10
* }
* }];
* }
* ```
*
* Note that the `required` rule does not apply. Map keys cannot be unset.
*
* @generated from field: optional buf.validate.FieldRules keys = 4;
*/
keys?: FieldRules;
/**
* Specifies the rules to be applied to the value of each key in the
* field. Message values will still have their validations evaluated unless
* `ignore` is specified.
*
* ```proto
* message MyMap {
* // The values in the field `value` must follow the specified rules.
* map value = 1 [(buf.validate.field).map.values = {
* string: {
* min_len: 5
* max_len: 20
* }
* }];
* }
* ```
* Note that the `required` rule does not apply. Map values cannot be unset.
*
* @generated from field: optional buf.validate.FieldRules values = 5;
*/
values?: FieldRules;
};
/**
* Describes the message buf.validate.MapRules.
* Use `create(MapRulesSchema)` to create a new message.
*/
export const MapRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 23);
/**
* AnyRules describe rules applied exclusively to the `google.protobuf.Any` well-known type.
*
* @generated from message buf.validate.AnyRules
*/
export type AnyRules = Message<"buf.validate.AnyRules"> & {
/**
* `in` requires the field's `type_url` to be equal to one of the
* specified values. If it doesn't match any of the specified values, an error
* message is generated.
*
* ```proto
* message MyAny {
* // The `value` field must have a `type_url` equal to one of the specified values.
* google.protobuf.Any value = 1 [(buf.validate.field).any = {
* in: ["type.googleapis.com/MyType1", "type.googleapis.com/MyType2"]
* }];
* }
* ```
*
* @generated from field: repeated string in = 2;
*/
in: string[];
/**
* requires the field's type_url to be not equal to any of the specified values. If it matches any of the specified values, an error message is generated.
*
* ```proto
* message MyAny {
* // The `value` field must not have a `type_url` equal to any of the specified values.
* google.protobuf.Any value = 1 [(buf.validate.field).any = {
* not_in: ["type.googleapis.com/ForbiddenType1", "type.googleapis.com/ForbiddenType2"]
* }];
* }
* ```
*
* @generated from field: repeated string not_in = 3;
*/
notIn: string[];
};
/**
* Describes the message buf.validate.AnyRules.
* Use `create(AnyRulesSchema)` to create a new message.
*/
export const AnyRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 24);
/**
* DurationRules describe the rules applied exclusively to the `google.protobuf.Duration` well-known type.
*
* @generated from message buf.validate.DurationRules
*/
export type DurationRules = Message<"buf.validate.DurationRules"> & {
/**
* `const` dictates that the field must match the specified value of the `google.protobuf.Duration` type exactly.
* If the field's value deviates from the specified value, an error message
* will be generated.
*
* ```proto
* message MyDuration {
* // value must equal 5s
* google.protobuf.Duration value = 1 [(buf.validate.field).duration.const = "5s"];
* }
* ```
*
* @generated from field: optional google.protobuf.Duration const = 2;
*/
const?: Duration;
/**
* @generated from oneof buf.validate.DurationRules.less_than
*/
lessThan: {
/**
* `lt` stipulates that the field must be less than the specified value of the `google.protobuf.Duration` type,
* exclusive. If the field's value is greater than or equal to the specified
* value, an error message will be generated.
*
* ```proto
* message MyDuration {
* // value must be less than 5s
* google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = "5s"];
* }
* ```
*
* @generated from field: google.protobuf.Duration lt = 3;
*/
value: Duration;
case: "lt";
} | {
/**
* `lte` indicates that the field must be less than or equal to the specified
* value of the `google.protobuf.Duration` type, inclusive. If the field's value is greater than the specified value,
* an error message will be generated.
*
* ```proto
* message MyDuration {
* // value must be less than or equal to 10s
* google.protobuf.Duration value = 1 [(buf.validate.field).duration.lte = "10s"];
* }
* ```
*
* @generated from field: google.protobuf.Duration lte = 4;
*/
value: Duration;
case: "lte";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.DurationRules.greater_than
*/
greaterThan: {
/**
* `gt` requires the duration field value to be greater than the specified
* value (exclusive). If the value of `gt` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyDuration {
* // duration must be greater than 5s [duration.gt]
* google.protobuf.Duration value = 1 [(buf.validate.field).duration.gt = { seconds: 5 }];
*
* // duration must be greater than 5s and less than 10s [duration.gt_lt]
* google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gt: { seconds: 5 }, lt: { seconds: 10 } }];
*
* // duration must be greater than 10s or less than 5s [duration.gt_lt_exclusive]
* google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gt: { seconds: 10 }, lt: { seconds: 5 } }];
* }
* ```
*
* @generated from field: google.protobuf.Duration gt = 5;
*/
value: Duration;
case: "gt";
} | {
/**
* `gte` requires the duration field value to be greater than or equal to the
* specified value (exclusive). If the value of `gte` is larger than a
* specified `lt` or `lte`, the range is reversed, and the field value must
* be outside the specified range. If the field value doesn't meet the
* required conditions, an error message is generated.
*
* ```proto
* message MyDuration {
* // duration must be greater than or equal to 5s [duration.gte]
* google.protobuf.Duration value = 1 [(buf.validate.field).duration.gte = { seconds: 5 }];
*
* // duration must be greater than or equal to 5s and less than 10s [duration.gte_lt]
* google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gte: { seconds: 5 }, lt: { seconds: 10 } }];
*
* // duration must be greater than or equal to 10s or less than 5s [duration.gte_lt_exclusive]
* google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gte: { seconds: 10 }, lt: { seconds: 5 } }];
* }
* ```
*
* @generated from field: google.protobuf.Duration gte = 6;
*/
value: Duration;
case: "gte";
} | { case: undefined; value?: undefined };
/**
* `in` asserts that the field must be equal to one of the specified values of the `google.protobuf.Duration` type.
* If the field's value doesn't correspond to any of the specified values,
* an error message will be generated.
*
* ```proto
* message MyDuration {
* // value must be in list [1s, 2s, 3s]
* google.protobuf.Duration value = 1 [(buf.validate.field).duration.in = ["1s", "2s", "3s"]];
* }
* ```
*
* @generated from field: repeated google.protobuf.Duration in = 7;
*/
in: Duration[];
/**
* `not_in` denotes that the field must not be equal to
* any of the specified values of the `google.protobuf.Duration` type.
* If the field's value matches any of these values, an error message will be
* generated.
*
* ```proto
* message MyDuration {
* // value must not be in list [1s, 2s, 3s]
* google.protobuf.Duration value = 1 [(buf.validate.field).duration.not_in = ["1s", "2s", "3s"]];
* }
* ```
*
* @generated from field: repeated google.protobuf.Duration not_in = 8;
*/
notIn: Duration[];
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyDuration {
* google.protobuf.Duration value = 1 [
* (buf.validate.field).duration.example = { seconds: 1 },
* (buf.validate.field).duration.example = { seconds: 2 },
* ];
* }
* ```
*
* @generated from field: repeated google.protobuf.Duration example = 9;
*/
example: Duration[];
};
/**
* Describes the message buf.validate.DurationRules.
* Use `create(DurationRulesSchema)` to create a new message.
*/
export const DurationRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 25);
/**
* TimestampRules describe the rules applied exclusively to the `google.protobuf.Timestamp` well-known type.
*
* @generated from message buf.validate.TimestampRules
*/
export type TimestampRules = Message<"buf.validate.TimestampRules"> & {
/**
* `const` dictates that this field, of the `google.protobuf.Timestamp` type, must exactly match the specified value. If the field value doesn't correspond to the specified timestamp, an error message will be generated.
*
* ```proto
* message MyTimestamp {
* // value must equal 2023-05-03T10:00:00Z
* google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.const = {seconds: 1727998800}];
* }
* ```
*
* @generated from field: optional google.protobuf.Timestamp const = 2;
*/
const?: Timestamp;
/**
* @generated from oneof buf.validate.TimestampRules.less_than
*/
lessThan: {
/**
* requires the duration field value to be less than the specified value (field < value). If the field value doesn't meet the required conditions, an error message is generated.
*
* ```proto
* message MyDuration {
* // duration must be less than 'P3D' [duration.lt]
* google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = { seconds: 259200 }];
* }
* ```
*
* @generated from field: google.protobuf.Timestamp lt = 3;
*/
value: Timestamp;
case: "lt";
} | {
/**
* requires the timestamp field value to be less than or equal to the specified value (field <= value). If the field value doesn't meet the required conditions, an error message is generated.
*
* ```proto
* message MyTimestamp {
* // timestamp must be less than or equal to '2023-05-14T00:00:00Z' [timestamp.lte]
* google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.lte = { seconds: 1678867200 }];
* }
* ```
*
* @generated from field: google.protobuf.Timestamp lte = 4;
*/
value: Timestamp;
case: "lte";
} | {
/**
* `lt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be less than the current time. `lt_now` can only be used with the `within` rule.
*
* ```proto
* message MyTimestamp {
* // value must be less than now
* google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true];
* }
* ```
*
* @generated from field: bool lt_now = 7;
*/
value: boolean;
case: "ltNow";
} | { case: undefined; value?: undefined };
/**
* @generated from oneof buf.validate.TimestampRules.greater_than
*/
greaterThan: {
/**
* `gt` requires the timestamp field value to be greater than the specified
* value (exclusive). If the value of `gt` is larger than a specified `lt`
* or `lte`, the range is reversed, and the field value must be outside the
* specified range. If the field value doesn't meet the required conditions,
* an error message is generated.
*
* ```proto
* message MyTimestamp {
* // timestamp must be greater than '2023-01-01T00:00:00Z' [timestamp.gt]
* google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gt = { seconds: 1672444800 }];
*
* // timestamp must be greater than '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gt_lt]
* google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gt: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }];
*
* // timestamp must be greater than '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gt_lt_exclusive]
* google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gt: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }];
* }
* ```
*
* @generated from field: google.protobuf.Timestamp gt = 5;
*/
value: Timestamp;
case: "gt";
} | {
/**
* `gte` requires the timestamp field value to be greater than or equal to the
* specified value (exclusive). If the value of `gte` is larger than a
* specified `lt` or `lte`, the range is reversed, and the field value
* must be outside the specified range. If the field value doesn't meet
* the required conditions, an error message is generated.
*
* ```proto
* message MyTimestamp {
* // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' [timestamp.gte]
* google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gte = { seconds: 1672444800 }];
*
* // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gte_lt]
* google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gte: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }];
*
* // timestamp must be greater than or equal to '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gte_lt_exclusive]
* google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gte: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }];
* }
* ```
*
* @generated from field: google.protobuf.Timestamp gte = 6;
*/
value: Timestamp;
case: "gte";
} | {
/**
* `gt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be greater than the current time. `gt_now` can only be used with the `within` rule.
*
* ```proto
* message MyTimestamp {
* // value must be greater than now
* google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.gt_now = true];
* }
* ```
*
* @generated from field: bool gt_now = 8;
*/
value: boolean;
case: "gtNow";
} | { case: undefined; value?: undefined };
/**
* `within` specifies that this field, of the `google.protobuf.Timestamp` type, must be within the specified duration of the current time. If the field value isn't within the duration, an error message is generated.
*
* ```proto
* message MyTimestamp {
* // value must be within 1 hour of now
* google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.within = {seconds: 3600}];
* }
* ```
*
* @generated from field: optional google.protobuf.Duration within = 9;
*/
within?: Duration;
/**
* `example` specifies values that the field may have. These values SHOULD
* conform to other rules. `example` values will not impact validation
* but may be used as helpful guidance on how to populate the given field.
*
* ```proto
* message MyTimestamp {
* google.protobuf.Timestamp value = 1 [
* (buf.validate.field).timestamp.example = { seconds: 1672444800 },
* (buf.validate.field).timestamp.example = { seconds: 1672531200 },
* ];
* }
* ```
*
* @generated from field: repeated google.protobuf.Timestamp example = 10;
*/
example: Timestamp[];
};
/**
* Describes the message buf.validate.TimestampRules.
* Use `create(TimestampRulesSchema)` to create a new message.
*/
export const TimestampRulesSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 26);
/**
* `Violations` is a collection of `Violation` messages. This message type is returned by
* Protovalidate when a proto message fails to meet the requirements set by the `Rule` validation rules.
* Each individual violation is represented by a `Violation` message.
*
* @generated from message buf.validate.Violations
*/
export type Violations = Message<"buf.validate.Violations"> & {
/**
* `violations` is a repeated field that contains all the `Violation` messages corresponding to the violations detected.
*
* @generated from field: repeated buf.validate.Violation violations = 1;
*/
violations: Violation[];
};
/**
* Describes the message buf.validate.Violations.
* Use `create(ViolationsSchema)` to create a new message.
*/
export const ViolationsSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 27);
/**
* `Violation` represents a single instance where a validation rule, expressed
* as a `Rule`, was not met. It provides information about the field that
* caused the violation, the specific rule that wasn't fulfilled, and a
* human-readable error message.
*
* For example, consider the following message:
*
* ```proto
* message User {
* int32 age = 1 [(buf.validate.field).cel = {
* id: "user.age",
* expression: "this < 18 ? 'User must be at least 18 years old' : ''",
* }];
* }
* ```
*
* It could produce the following violation:
*
* ```json
* {
* "ruleId": "user.age",
* "message": "User must be at least 18 years old",
* "field": {
* "elements": [
* {
* "fieldNumber": 1,
* "fieldName": "age",
* "fieldType": "TYPE_INT32"
* }
* ]
* },
* "rule": {
* "elements": [
* {
* "fieldNumber": 23,
* "fieldName": "cel",
* "fieldType": "TYPE_MESSAGE",
* "index": "0"
* }
* ]
* }
* }
* ```
*
* @generated from message buf.validate.Violation
*/
export type Violation = Message<"buf.validate.Violation"> & {
/**
* `field` is a machine-readable path to the field that failed validation.
* This could be a nested field, in which case the path will include all the parent fields leading to the actual field that caused the violation.
*
* For example, consider the following message:
*
* ```proto
* message Message {
* bool a = 1 [(buf.validate.field).required = true];
* }
* ```
*
* It could produce the following violation:
*
* ```textproto
* violation {
* field { element { field_number: 1, field_name: "a", field_type: 8 } }
* ...
* }
* ```
*
* @generated from field: optional buf.validate.FieldPath field = 5;
*/
field?: FieldPath;
/**
* `rule` is a machine-readable path that points to the specific rule that failed validation.
* This will be a nested field starting from the FieldRules of the field that failed validation.
* For custom rules, this will provide the path of the rule, e.g. `cel[0]`.
*
* For example, consider the following message:
*
* ```proto
* message Message {
* bool a = 1 [(buf.validate.field).required = true];
* bool b = 2 [(buf.validate.field).cel = {
* id: "custom_rule",
* expression: "!this ? 'b must be true': ''"
* }]
* }
* ```
*
* It could produce the following violations:
*
* ```textproto
* violation {
* rule { element { field_number: 25, field_name: "required", field_type: 8 } }
* ...
* }
* violation {
* rule { element { field_number: 23, field_name: "cel", field_type: 11, index: 0 } }
* ...
* }
* ```
*
* @generated from field: optional buf.validate.FieldPath rule = 6;
*/
rule?: FieldPath;
/**
* `rule_id` is the unique identifier of the `Rule` that was not fulfilled.
* This is the same `id` that was specified in the `Rule` message, allowing easy tracing of which rule was violated.
*
* @generated from field: optional string rule_id = 2;
*/
ruleId: string;
/**
* `message` is a human-readable error message that describes the nature of the violation.
* This can be the default error message from the violated `Rule`, or it can be a custom message that gives more context about the violation.
*
* @generated from field: optional string message = 3;
*/
message: string;
/**
* `for_key` indicates whether the violation was caused by a map key, rather than a value.
*
* @generated from field: optional bool for_key = 4;
*/
forKey: boolean;
};
/**
* Describes the message buf.validate.Violation.
* Use `create(ViolationSchema)` to create a new message.
*/
export const ViolationSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 28);
/**
* `FieldPath` provides a path to a nested protobuf field.
*
* This message provides enough information to render a dotted field path even without protobuf descriptors.
* It also provides enough information to resolve a nested field through unknown wire data.
*
* @generated from message buf.validate.FieldPath
*/
export type FieldPath = Message<"buf.validate.FieldPath"> & {
/**
* `elements` contains each element of the path, starting from the root and recursing downward.
*
* @generated from field: repeated buf.validate.FieldPathElement elements = 1;
*/
elements: FieldPathElement[];
};
/**
* Describes the message buf.validate.FieldPath.
* Use `create(FieldPathSchema)` to create a new message.
*/
export const FieldPathSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 29);
/**
* `FieldPathElement` provides enough information to nest through a single protobuf field.
*
* If the selected field is a map or repeated field, the `subscript` value selects a specific element from it.
* A path that refers to a value nested under a map key or repeated field index will have a `subscript` value.
* The `field_type` field allows unambiguous resolution of a field even if descriptors are not available.
*
* @generated from message buf.validate.FieldPathElement
*/
export type FieldPathElement = Message<"buf.validate.FieldPathElement"> & {
/**
* `field_number` is the field number this path element refers to.
*
* @generated from field: optional int32 field_number = 1;
*/
fieldNumber: number;
/**
* `field_name` contains the field name this path element refers to.
* This can be used to display a human-readable path even if the field number is unknown.
*
* @generated from field: optional string field_name = 2;
*/
fieldName: string;
/**
* `field_type` specifies the type of this field. When using reflection, this value is not needed.
*
* This value is provided to make it possible to traverse unknown fields through wire data.
* When traversing wire data, be mindful of both packed[1] and delimited[2] encoding schemes.
*
* [1]: https://protobuf.dev/programming-guides/encoding/#packed
* [2]: https://protobuf.dev/programming-guides/encoding/#groups
*
* N.B.: Although groups are deprecated, the corresponding delimited encoding scheme is not, and
* can be explicitly used in Protocol Buffers 2023 Edition.
*
* @generated from field: optional google.protobuf.FieldDescriptorProto.Type field_type = 3;
*/
fieldType: FieldDescriptorProto_Type;
/**
* `key_type` specifies the map key type of this field. This value is useful when traversing
* unknown fields through wire data: specifically, it allows handling the differences between
* different integer encodings.
*
* @generated from field: optional google.protobuf.FieldDescriptorProto.Type key_type = 4;
*/
keyType: FieldDescriptorProto_Type;
/**
* `value_type` specifies map value type of this field. This is useful if you want to display a
* value inside unknown fields through wire data.
*
* @generated from field: optional google.protobuf.FieldDescriptorProto.Type value_type = 5;
*/
valueType: FieldDescriptorProto_Type;
/**
* `subscript` contains a repeated index or map key, if this path element nests into a repeated or map field.
*
* @generated from oneof buf.validate.FieldPathElement.subscript
*/
subscript: {
/**
* `index` specifies a 0-based index into a repeated field.
*
* @generated from field: uint64 index = 6;
*/
value: bigint;
case: "index";
} | {
/**
* `bool_key` specifies a map key of type bool.
*
* @generated from field: bool bool_key = 7;
*/
value: boolean;
case: "boolKey";
} | {
/**
* `int_key` specifies a map key of type int32, int64, sint32, sint64, sfixed32 or sfixed64.
*
* @generated from field: int64 int_key = 8;
*/
value: bigint;
case: "intKey";
} | {
/**
* `uint_key` specifies a map key of type uint32, uint64, fixed32 or fixed64.
*
* @generated from field: uint64 uint_key = 9;
*/
value: bigint;
case: "uintKey";
} | {
/**
* `string_key` specifies a map key of type string.
*
* @generated from field: string string_key = 10;
*/
value: string;
case: "stringKey";
} | { case: undefined; value?: undefined };
};
/**
* Describes the message buf.validate.FieldPathElement.
* Use `create(FieldPathElementSchema)` to create a new message.
*/
export const FieldPathElementSchema: GenMessage = /*@__PURE__*/
messageDesc(file_buf_validate_validate, 30);
/**
* Specifies how `FieldRules.ignore` behaves, depending on the field's value, and
* whether the field tracks presence.
*
* @generated from enum buf.validate.Ignore
*/
export enum Ignore {
/**
* Ignore rules if the field tracks presence and is unset. This is the default
* behavior.
*
* In proto3, only message fields, members of a Protobuf `oneof`, and fields
* with the `optional` label track presence. Consequently, the following fields
* are always validated, whether a value is set or not:
*
* ```proto
* syntax="proto3";
*
* message RulesApply {
* string email = 1 [
* (buf.validate.field).string.email = true
* ];
* int32 age = 2 [
* (buf.validate.field).int32.gt = 0
* ];
* repeated string labels = 3 [
* (buf.validate.field).repeated.min_items = 1
* ];
* }
* ```
*
* In contrast, the following fields track presence, and are only validated if
* a value is set:
*
* ```proto
* syntax="proto3";
*
* message RulesApplyIfSet {
* optional string email = 1 [
* (buf.validate.field).string.email = true
* ];
* oneof ref {
* string reference = 2 [
* (buf.validate.field).string.uuid = true
* ];
* string name = 3 [
* (buf.validate.field).string.min_len = 4
* ];
* }
* SomeMessage msg = 4 [
* (buf.validate.field).cel = {/* ... *\/}
* ];
* }
* ```
*
* To ensure that such a field is set, add the `required` rule.
*
* To learn which fields track presence, see the
* [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat).
*
* @generated from enum value: IGNORE_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* Ignore rules if the field is unset, or set to the zero value.
*
* The zero value depends on the field type:
* - For strings, the zero value is the empty string.
* - For bytes, the zero value is empty bytes.
* - For bool, the zero value is false.
* - For numeric types, the zero value is zero.
* - For enums, the zero value is the first defined enum value.
* - For repeated fields, the zero is an empty list.
* - For map fields, the zero is an empty map.
* - For message fields, absence of the message (typically a null-value) is considered zero value.
*
* For fields that track presence (e.g. adding the `optional` label in proto3),
* this a no-op and behavior is the same as the default `IGNORE_UNSPECIFIED`.
*
* @generated from enum value: IGNORE_IF_ZERO_VALUE = 1;
*/
IF_ZERO_VALUE = 1,
/**
* Always ignore rules, including the `required` rule.
*
* This is useful for ignoring the rules of a referenced message, or to
* temporarily ignore rules during development.
*
* ```proto
* message MyMessage {
* // The field's rules will always be ignored, including any validations
* // on value's fields.
* MyOtherMessage value = 1 [
* (buf.validate.field).ignore = IGNORE_ALWAYS
* ];
* }
* ```
*
* @generated from enum value: IGNORE_ALWAYS = 3;
*/
ALWAYS = 3,
}
/**
* Describes the enum buf.validate.Ignore.
*/
export const IgnoreSchema: GenEnum = /*@__PURE__*/
enumDesc(file_buf_validate_validate, 0);
/**
* KnownRegex contains some well-known patterns.
*
* @generated from enum buf.validate.KnownRegex
*/
export enum KnownRegex {
/**
* @generated from enum value: KNOWN_REGEX_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2).
*
* @generated from enum value: KNOWN_REGEX_HTTP_HEADER_NAME = 1;
*/
HTTP_HEADER_NAME = 1,
/**
* HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4).
*
* @generated from enum value: KNOWN_REGEX_HTTP_HEADER_VALUE = 2;
*/
HTTP_HEADER_VALUE = 2,
}
/**
* Describes the enum buf.validate.KnownRegex.
*/
export const KnownRegexSchema: GenEnum = /*@__PURE__*/
enumDesc(file_buf_validate_validate, 1);
/**
* Rules specify the validations to be performed on this message. By default,
* no validation is performed against a message.
*
* @generated from extension: optional buf.validate.MessageRules message = 1159;
*/
export const message: GenExtension = /*@__PURE__*/
extDesc(file_buf_validate_validate, 0);
/**
* Rules specify the validations to be performed on this oneof. By default,
* no validation is performed against a oneof.
*
* @generated from extension: optional buf.validate.OneofRules oneof = 1159;
*/
export const oneof: GenExtension = /*@__PURE__*/
extDesc(file_buf_validate_validate, 1);
/**
* Rules specify the validations to be performed on this field. By default,
* no validation is performed against a field.
*
* @generated from extension: optional buf.validate.FieldRules field = 1159;
*/
export const field: GenExtension = /*@__PURE__*/
extDesc(file_buf_validate_validate, 2);
/**
* Specifies predefined rules. When extending a standard rule message,
* this adds additional CEL expressions that apply when the extension is used.
*
* ```proto
* extend buf.validate.Int32Rules {
* bool is_zero [(buf.validate.predefined).cel = {
* id: "int32.is_zero",
* message: "value must be zero",
* expression: "!rule || this == 0",
* }];
* }
*
* message Foo {
* int32 reserved = 1 [(buf.validate.field).int32.(is_zero) = true];
* }
* ```
*
* @generated from extension: optional buf.validate.PredefinedRules predefined = 1160;
*/
export const predefined: GenExtension = /*@__PURE__*/
extDesc(file_buf_validate_validate, 3);
================================================
FILE: app/generated/ito_connect.ts
================================================
// @generated by protoc-gen-connect-es v1.6.1 with parameter "target=ts,import_extension=.js"
// @generated from file ito.proto (package ito, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import { AdvancedSettings, AudioChunk, CreateDictionaryItemRequest, CreateInteractionRequest, CreateNoteRequest, DeleteDictionaryItemRequest, DeleteInteractionRequest, DeleteNoteRequest, DeleteUserDataRequest, DictionaryItem, Empty, GetAdvancedSettingsRequest, GetInteractionRequest, GetNoteRequest, Interaction, ListDictionaryItemsRequest, ListDictionaryItemsResponse, ListInteractionsRequest, ListInteractionsResponse, ListNotesRequest, ListNotesResponse, Note, SubmitTimingReportsRequest, SubmitTimingReportsResponse, TranscribeStreamRequest, TranscriptionResponse, UpdateAdvancedSettingsRequest, UpdateDictionaryItemRequest, UpdateInteractionRequest, UpdateNoteRequest } from "./ito_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
/**
* @generated from service ito.ItoService
*/
export const ItoService = {
typeName: "ito.ItoService",
methods: {
/**
* Streams audio chunks from the client and gets a single response.
* This is the ideal method for dictation to reduce latency and memory usage.
*
* @generated from rpc ito.ItoService.TranscribeStream
*/
transcribeStream: {
name: "TranscribeStream",
I: AudioChunk,
O: TranscriptionResponse,
kind: MethodKind.ClientStreaming,
},
/**
* Enhanced streaming transcription that accepts configuration data in-stream.
* Config can be sent before, during, or omitted entirely. Multiple config messages
* are merged by the server. This allows immediate streaming without waiting for context.
*
* @generated from rpc ito.ItoService.TranscribeStreamV2
*/
transcribeStreamV2: {
name: "TranscribeStreamV2",
I: TranscribeStreamRequest,
O: TranscriptionResponse,
kind: MethodKind.ClientStreaming,
},
/**
* Note Service
*
* @generated from rpc ito.ItoService.CreateNote
*/
createNote: {
name: "CreateNote",
I: CreateNoteRequest,
O: Note,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.GetNote
*/
getNote: {
name: "GetNote",
I: GetNoteRequest,
O: Note,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.ListNotes
*/
listNotes: {
name: "ListNotes",
I: ListNotesRequest,
O: ListNotesResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.UpdateNote
*/
updateNote: {
name: "UpdateNote",
I: UpdateNoteRequest,
O: Note,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.DeleteNote
*/
deleteNote: {
name: "DeleteNote",
I: DeleteNoteRequest,
O: Empty,
kind: MethodKind.Unary,
},
/**
* Interaction Service
*
* @generated from rpc ito.ItoService.CreateInteraction
*/
createInteraction: {
name: "CreateInteraction",
I: CreateInteractionRequest,
O: Interaction,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.GetInteraction
*/
getInteraction: {
name: "GetInteraction",
I: GetInteractionRequest,
O: Interaction,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.ListInteractions
*/
listInteractions: {
name: "ListInteractions",
I: ListInteractionsRequest,
O: ListInteractionsResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.UpdateInteraction
*/
updateInteraction: {
name: "UpdateInteraction",
I: UpdateInteractionRequest,
O: Interaction,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.DeleteInteraction
*/
deleteInteraction: {
name: "DeleteInteraction",
I: DeleteInteractionRequest,
O: Empty,
kind: MethodKind.Unary,
},
/**
* Dictionary Service
*
* @generated from rpc ito.ItoService.CreateDictionaryItem
*/
createDictionaryItem: {
name: "CreateDictionaryItem",
I: CreateDictionaryItemRequest,
O: DictionaryItem,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.ListDictionaryItems
*/
listDictionaryItems: {
name: "ListDictionaryItems",
I: ListDictionaryItemsRequest,
O: ListDictionaryItemsResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.UpdateDictionaryItem
*/
updateDictionaryItem: {
name: "UpdateDictionaryItem",
I: UpdateDictionaryItemRequest,
O: DictionaryItem,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.DeleteDictionaryItem
*/
deleteDictionaryItem: {
name: "DeleteDictionaryItem",
I: DeleteDictionaryItemRequest,
O: Empty,
kind: MethodKind.Unary,
},
/**
* User Data Service
*
* @generated from rpc ito.ItoService.DeleteUserData
*/
deleteUserData: {
name: "DeleteUserData",
I: DeleteUserDataRequest,
O: Empty,
kind: MethodKind.Unary,
},
/**
* Advanced Settings Service
*
* @generated from rpc ito.ItoService.GetAdvancedSettings
*/
getAdvancedSettings: {
name: "GetAdvancedSettings",
I: GetAdvancedSettingsRequest,
O: AdvancedSettings,
kind: MethodKind.Unary,
},
/**
* @generated from rpc ito.ItoService.UpdateAdvancedSettings
*/
updateAdvancedSettings: {
name: "UpdateAdvancedSettings",
I: UpdateAdvancedSettingsRequest,
O: AdvancedSettings,
kind: MethodKind.Unary,
},
}
} as const;
/**
* @generated from service ito.TimingService
*/
export const TimingService = {
typeName: "ito.TimingService",
methods: {
/**
* Submit timing reports for interaction analytics
*
* @generated from rpc ito.TimingService.SubmitTimingReports
*/
submitTimingReports: {
name: "SubmitTimingReports",
I: SubmitTimingReportsRequest,
O: SubmitTimingReportsResponse,
kind: MethodKind.Unary,
},
}
} as const;
================================================
FILE: app/generated/ito_pb.ts
================================================
// @generated by protoc-gen-es v2.7.0 with parameter "target=ts,import_extension=.js"
// @generated from file ito.proto (package ito, syntax proto3)
/* eslint-disable */
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import { file_buf_validate_validate } from "./buf/validate/validate_pb.js";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file ito.proto.
*/
export const file_ito: GenFile = /*@__PURE__*/
fileDesc("CglpdG8ucHJvdG8SA2l0byIHCgVFbXB0eSLRAQoLQ2xpZW50RXJyb3ISDAoEY29kZRgBIAEoCRIcCgR0eXBlGAIgASgOMg4uaXRvLkVycm9yVHlwZRIPCgdtZXNzYWdlGAMgASgJEiUKCHByb3ZpZGVyGAQgASgOMhMuaXRvLkNsaWVudFByb3ZpZGVyEi4KB2RldGFpbHMYBSADKAsyHS5pdG8uQ2xpZW50RXJyb3IuRGV0YWlsc0VudHJ5Gi4KDERldGFpbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIisKCkF1ZGlvQ2h1bmsSHQoKYXVkaW9fZGF0YRgBIAEoDEIJukgGegQYgIBAIrMBCgtDb250ZXh0SW5mbxIZCgx3aW5kb3dfdGl0bGUYASABKAlIAIgBARIVCghhcHBfbmFtZRgCIAEoCUgBiAEBEhkKDGNvbnRleHRfdGV4dBgDIAEoCUgCiAEBEh8KBG1vZGUYBCABKA4yDC5pdG8uSXRvTW9kZUgDiAEBQg8KDV93aW5kb3dfdGl0bGVCCwoJX2FwcF9uYW1lQg8KDV9jb250ZXh0X3RleHRCBwoFX21vZGUixAEKDFN0cmVhbUNvbmZpZxImCgdjb250ZXh0GAEgASgLMhAuaXRvLkNvbnRleHRJbmZvSACIAQESKwoMbGxtX3NldHRpbmdzGAIgASgLMhAuaXRvLkxsbVNldHRpbmdzSAGIAQESEgoKdm9jYWJ1bGFyeRgDIAMoCRIbCg5pbnRlcmFjdGlvbl9pZBgEIAEoCUgCiAEBQgoKCF9jb250ZXh0Qg8KDV9sbG1fc2V0dGluZ3NCEQoPX2ludGVyYWN0aW9uX2lkImoKF1RyYW5zY3JpYmVTdHJlYW1SZXF1ZXN0EiMKBmNvbmZpZxgBIAEoCzIRLml0by5TdHJlYW1Db25maWdIABIfCgphdWRpb19kYXRhGAIgASgMQgm6SAZ6BBiAgEBIAEIJCgdwYXlsb2FkIkwKFVRyYW5zY3JpcHRpb25SZXNwb25zZRISCgp0cmFuc2NyaXB0GAEgASgJEh8KBWVycm9yGAIgASgLMhAuaXRvLkNsaWVudEVycm9yIogBCgROb3RlEgoKAmlkGAEgASgJEg8KB3VzZXJfaWQYAiABKAkSFgoOaW50ZXJhY3Rpb25faWQYAyABKAkSDwoHY29udGVudBgEIAEoCRISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAkSEgoKZGVsZXRlZF9hdBgHIAEoCSJIChFDcmVhdGVOb3RlUmVxdWVzdBIKCgJpZBgBIAEoCRIWCg5pbnRlcmFjdGlvbl9pZBgCIAEoCRIPCgdjb250ZW50GAMgASgJIhwKDkdldE5vdGVSZXF1ZXN0EgoKAmlkGAEgASgJIisKEExpc3ROb3Rlc1JlcXVlc3QSFwoPc2luY2VfdGltZXN0YW1wGAEgASgJIi0KEUxpc3ROb3Rlc1Jlc3BvbnNlEhgKBW5vdGVzGAEgAygLMgkuaXRvLk5vdGUiMAoRVXBkYXRlTm90ZVJlcXVlc3QSCgoCaWQYASABKAkSDwoHY29udGVudBgCIAEoCSIfChFEZWxldGVOb3RlUmVxdWVzdBIKCgJpZBgBIAEoCSL9AQoLSW50ZXJhY3Rpb24SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRISCgphc3Jfb3V0cHV0GAQgASgJEhIKCmxsbV9vdXRwdXQYBSABKAkSHQoJcmF3X2F1ZGlvGAYgASgMQgq6SAd6BRiAwtcvEhMKC2R1cmF0aW9uX21zGAcgASgFEhIKCmNyZWF0ZWRfYXQYCCABKAkSEgoKdXBkYXRlZF9hdBgJIAEoCRISCgpkZWxldGVkX2F0GAogASgJEhkKDHJhd19hdWRpb19pZBgLIAEoCUgAiAEBQg8KDV9yYXdfYXVkaW9faWQikQEKGENyZWF0ZUludGVyYWN0aW9uUmVxdWVzdBIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgphc3Jfb3V0cHV0GAMgASgJEhIKCmxsbV9vdXRwdXQYBCABKAkSHQoJcmF3X2F1ZGlvGAUgASgMQgq6SAd6BRiAwtcvEhMKC2R1cmF0aW9uX21zGAYgASgFIiMKFUdldEludGVyYWN0aW9uUmVxdWVzdBIKCgJpZBgBIAEoCSIyChdMaXN0SW50ZXJhY3Rpb25zUmVxdWVzdBIXCg9zaW5jZV90aW1lc3RhbXAYASABKAkiQgoYTGlzdEludGVyYWN0aW9uc1Jlc3BvbnNlEiYKDGludGVyYWN0aW9ucxgBIAMoCzIQLml0by5JbnRlcmFjdGlvbiI1ChhVcGRhdGVJbnRlcmFjdGlvblJlcXVlc3QSCgoCaWQYASABKAkSDQoFdGl0bGUYAiABKAkiJgoYRGVsZXRlSW50ZXJhY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIo4BCg5EaWN0aW9uYXJ5SXRlbRIKCgJpZBgBIAEoCRIPCgd1c2VyX2lkGAIgASgJEgwKBHdvcmQYAyABKAkSFQoNcHJvbnVuY2lhdGlvbhgEIAEoCRISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAkSEgoKZGVsZXRlZF9hdBgHIAEoCSJOChtDcmVhdGVEaWN0aW9uYXJ5SXRlbVJlcXVlc3QSCgoCaWQYASABKAkSDAoEd29yZBgCIAEoCRIVCg1wcm9udW5jaWF0aW9uGAMgASgJIjUKGkxpc3REaWN0aW9uYXJ5SXRlbXNSZXF1ZXN0EhcKD3NpbmNlX3RpbWVzdGFtcBgBIAEoCSJBChtMaXN0RGljdGlvbmFyeUl0ZW1zUmVzcG9uc2USIgoFaXRlbXMYASADKAsyEy5pdG8uRGljdGlvbmFyeUl0ZW0iTgobVXBkYXRlRGljdGlvbmFyeUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgJEgwKBHdvcmQYAiABKAkSFQoNcHJvbnVuY2lhdGlvbhgDIAEoCSIpChtEZWxldGVEaWN0aW9uYXJ5SXRlbVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlVXNlckRhdGFSZXF1ZXN0Iu8DCgtMbG1TZXR0aW5ncxIWCglhc3JfbW9kZWwYASABKAlIAIgBARIZCgxhc3JfcHJvdmlkZXIYAiABKAlIAYgBARIXCgphc3JfcHJvbXB0GAMgASgJSAKIAQESGQoMbGxtX3Byb3ZpZGVyGAQgASgJSAOIAQESFgoJbGxtX21vZGVsGAUgASgJSASIAQESHAoPbGxtX3RlbXBlcmF0dXJlGAYgASgCSAWIAQESIQoUdHJhbnNjcmlwdGlvbl9wcm9tcHQYByABKAlIBogBARIbCg5lZGl0aW5nX3Byb21wdBgIIAEoCUgHiAEBEiAKE25vX3NwZWVjaF90aHJlc2hvbGQYCSABKAJICIgBARIiChVsb3dfcXVhbGl0eV90aHJlc2hvbGQYCiABKAJICYgBAUIMCgpfYXNyX21vZGVsQg8KDV9hc3JfcHJvdmlkZXJCDQoLX2Fzcl9wcm9tcHRCDwoNX2xsbV9wcm92aWRlckIMCgpfbGxtX21vZGVsQhIKEF9sbG1fdGVtcGVyYXR1cmVCFwoVX3RyYW5zY3JpcHRpb25fcHJvbXB0QhEKD19lZGl0aW5nX3Byb21wdEIWChRfbm9fc3BlZWNoX3RocmVzaG9sZEIYChZfbG93X3F1YWxpdHlfdGhyZXNob2xkIpkBChBBZHZhbmNlZFNldHRpbmdzEgoKAmlkGAEgASgJEg8KB3VzZXJfaWQYAiABKAkSEgoKY3JlYXRlZF9hdBgDIAEoCRISCgp1cGRhdGVkX2F0GAQgASgJEh0KA2xsbRgFIAEoCzIQLml0by5MbG1TZXR0aW5ncxIhCgdkZWZhdWx0GAYgASgLMhAuaXRvLkxsbVNldHRpbmdzIhwKGkdldEFkdmFuY2VkU2V0dGluZ3NSZXF1ZXN0Ij4KHVVwZGF0ZUFkdmFuY2VkU2V0dGluZ3NSZXF1ZXN0Eh0KA2xsbRgBIAEoCzIQLml0by5MbG1TZXR0aW5ncyJ3CgtUaW1pbmdFdmVudBIMCgRuYW1lGAEgASgJEhAKCHN0YXJ0X21zGAIgASgBEhMKBmVuZF9tcxgDIAEoAUgAiAEBEhgKC2R1cmF0aW9uX21zGAQgASgBSAGIAQFCCQoHX2VuZF9tc0IOCgxfZHVyYXRpb25fbXMi1gEKDFRpbWluZ1JlcG9ydBIWCg5pbnRlcmFjdGlvbl9pZBgBIAEoCRIPCgd1c2VyX2lkGAIgASgJEhAKCHBsYXRmb3JtGAMgASgJEhMKC2FwcF92ZXJzaW9uGAQgASgJEhAKCGhvc3RuYW1lGAUgASgJEhQKDGFyY2hpdGVjdHVyZRgGIAEoCRIRCgl0aW1lc3RhbXAYByABKAkSIAoGZXZlbnRzGAggAygLMhAuaXRvLlRpbWluZ0V2ZW50EhkKEXRvdGFsX2R1cmF0aW9uX21zGAkgASgBIkAKGlN1Ym1pdFRpbWluZ1JlcG9ydHNSZXF1ZXN0EiIKB3JlcG9ydHMYASADKAsyES5pdG8uVGltaW5nUmVwb3J0Ih0KG1N1Ym1pdFRpbWluZ1JlcG9ydHNSZXNwb25zZSojCgdJdG9Nb2RlEg4KClRSQU5TQ1JJQkUQABIICgRFRElUEAEqKAoOQ2xpZW50UHJvdmlkZXISCAoER1JPURAAEgwKCENFUkVCUkFTEAEqRAoJRXJyb3JUeXBlEhEKDUNPTkZJR1VSQVRJT04QABIQCgxBVkFJTEFCSUxJVFkQARIJCgVBVURJTxACEgcKA0FQSRADMpUKCgpJdG9TZXJ2aWNlEkEKEFRyYW5zY3JpYmVTdHJlYW0SDy5pdG8uQXVkaW9DaHVuaxoaLml0by5UcmFuc2NyaXB0aW9uUmVzcG9uc2UoARJQChJUcmFuc2NyaWJlU3RyZWFtVjISHC5pdG8uVHJhbnNjcmliZVN0cmVhbVJlcXVlc3QaGi5pdG8uVHJhbnNjcmlwdGlvblJlc3BvbnNlKAESLwoKQ3JlYXRlTm90ZRIWLml0by5DcmVhdGVOb3RlUmVxdWVzdBoJLml0by5Ob3RlEikKB0dldE5vdGUSEy5pdG8uR2V0Tm90ZVJlcXVlc3QaCS5pdG8uTm90ZRI6CglMaXN0Tm90ZXMSFS5pdG8uTGlzdE5vdGVzUmVxdWVzdBoWLml0by5MaXN0Tm90ZXNSZXNwb25zZRIvCgpVcGRhdGVOb3RlEhYuaXRvLlVwZGF0ZU5vdGVSZXF1ZXN0GgkuaXRvLk5vdGUSMAoKRGVsZXRlTm90ZRIWLml0by5EZWxldGVOb3RlUmVxdWVzdBoKLml0by5FbXB0eRJEChFDcmVhdGVJbnRlcmFjdGlvbhIdLml0by5DcmVhdGVJbnRlcmFjdGlvblJlcXVlc3QaEC5pdG8uSW50ZXJhY3Rpb24SPgoOR2V0SW50ZXJhY3Rpb24SGi5pdG8uR2V0SW50ZXJhY3Rpb25SZXF1ZXN0GhAuaXRvLkludGVyYWN0aW9uEk8KEExpc3RJbnRlcmFjdGlvbnMSHC5pdG8uTGlzdEludGVyYWN0aW9uc1JlcXVlc3QaHS5pdG8uTGlzdEludGVyYWN0aW9uc1Jlc3BvbnNlEkQKEVVwZGF0ZUludGVyYWN0aW9uEh0uaXRvLlVwZGF0ZUludGVyYWN0aW9uUmVxdWVzdBoQLml0by5JbnRlcmFjdGlvbhI+ChFEZWxldGVJbnRlcmFjdGlvbhIdLml0by5EZWxldGVJbnRlcmFjdGlvblJlcXVlc3QaCi5pdG8uRW1wdHkSTQoUQ3JlYXRlRGljdGlvbmFyeUl0ZW0SIC5pdG8uQ3JlYXRlRGljdGlvbmFyeUl0ZW1SZXF1ZXN0GhMuaXRvLkRpY3Rpb25hcnlJdGVtElgKE0xpc3REaWN0aW9uYXJ5SXRlbXMSHy5pdG8uTGlzdERpY3Rpb25hcnlJdGVtc1JlcXVlc3QaIC5pdG8uTGlzdERpY3Rpb25hcnlJdGVtc1Jlc3BvbnNlEk0KFFVwZGF0ZURpY3Rpb25hcnlJdGVtEiAuaXRvLlVwZGF0ZURpY3Rpb25hcnlJdGVtUmVxdWVzdBoTLml0by5EaWN0aW9uYXJ5SXRlbRJEChREZWxldGVEaWN0aW9uYXJ5SXRlbRIgLml0by5EZWxldGVEaWN0aW9uYXJ5SXRlbVJlcXVlc3QaCi5pdG8uRW1wdHkSOAoORGVsZXRlVXNlckRhdGESGi5pdG8uRGVsZXRlVXNlckRhdGFSZXF1ZXN0GgouaXRvLkVtcHR5Ek0KE0dldEFkdmFuY2VkU2V0dGluZ3MSHy5pdG8uR2V0QWR2YW5jZWRTZXR0aW5nc1JlcXVlc3QaFS5pdG8uQWR2YW5jZWRTZXR0aW5ncxJTChZVcGRhdGVBZHZhbmNlZFNldHRpbmdzEiIuaXRvLlVwZGF0ZUFkdmFuY2VkU2V0dGluZ3NSZXF1ZXN0GhUuaXRvLkFkdmFuY2VkU2V0dGluZ3MyaQoNVGltaW5nU2VydmljZRJYChNTdWJtaXRUaW1pbmdSZXBvcnRzEh8uaXRvLlN1Ym1pdFRpbWluZ1JlcG9ydHNSZXF1ZXN0GiAuaXRvLlN1Ym1pdFRpbWluZ1JlcG9ydHNSZXNwb25zZWIGcHJvdG8z", [file_buf_validate_validate]);
/**
* General
* -----------------------------------------------------------------
*
* @generated from message ito.Empty
*/
export type Empty = Message<"ito.Empty"> & {
};
/**
* Describes the message ito.Empty.
* Use `create(EmptySchema)` to create a new message.
*/
export const EmptySchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 0);
/**
* @generated from message ito.ClientError
*/
export type ClientError = Message<"ito.ClientError"> & {
/**
* @generated from field: string code = 1;
*/
code: string;
/**
* @generated from field: ito.ErrorType type = 2;
*/
type: ErrorType;
/**
* @generated from field: string message = 3;
*/
message: string;
/**
* @generated from field: ito.ClientProvider provider = 4;
*/
provider: ClientProvider;
/**
* @generated from field: map details = 5;
*/
details: { [key: string]: string };
};
/**
* Describes the message ito.ClientError.
* Use `create(ClientErrorSchema)` to create a new message.
*/
export const ClientErrorSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 1);
/**
* Transcription
* -----------------------------------------------------------------
* A chunk of audio data for streaming.
*
* @generated from message ito.AudioChunk
*/
export type AudioChunk = Message<"ito.AudioChunk"> & {
/**
* 1 MB limit per chunk
*
* @generated from field: bytes audio_data = 1;
*/
audioData: Uint8Array;
};
/**
* Describes the message ito.AudioChunk.
* Use `create(AudioChunkSchema)` to create a new message.
*/
export const AudioChunkSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 2);
/**
* Context information for transcription.
*
* @generated from message ito.ContextInfo
*/
export type ContextInfo = Message<"ito.ContextInfo"> & {
/**
* @generated from field: optional string window_title = 1;
*/
windowTitle?: string;
/**
* @generated from field: optional string app_name = 2;
*/
appName?: string;
/**
* @generated from field: optional string context_text = 3;
*/
contextText?: string;
/**
* @generated from field: optional ito.ItoMode mode = 4;
*/
mode?: ItoMode;
};
/**
* Describes the message ito.ContextInfo.
* Use `create(ContextInfoSchema)` to create a new message.
*/
export const ContextInfoSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 3);
/**
* Configuration that can be sent in-stream for TranscribeStreamV2.
* All fields are optional and will be merged by the server.
* Multiple config messages received during the stream are progressively merged.
*
* @generated from message ito.StreamConfig
*/
export type StreamConfig = Message<"ito.StreamConfig"> & {
/**
* @generated from field: optional ito.ContextInfo context = 1;
*/
context?: ContextInfo;
/**
* @generated from field: optional ito.LlmSettings llm_settings = 2;
*/
llmSettings?: LlmSettings;
/**
* @generated from field: repeated string vocabulary = 3;
*/
vocabulary: string[];
/**
* @generated from field: optional string interaction_id = 4;
*/
interactionId?: string;
};
/**
* Describes the message ito.StreamConfig.
* Use `create(StreamConfigSchema)` to create a new message.
*/
export const StreamConfigSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 4);
/**
* Request message for TranscribeStreamV2.
* Can contain either configuration or audio data.
*
* @generated from message ito.TranscribeStreamRequest
*/
export type TranscribeStreamRequest = Message<"ito.TranscribeStreamRequest"> & {
/**
* @generated from oneof ito.TranscribeStreamRequest.payload
*/
payload: {
/**
* Configuration/context data
*
* @generated from field: ito.StreamConfig config = 1;
*/
value: StreamConfig;
case: "config";
} | {
/**
* Audio chunk (1 MB limit)
*
* @generated from field: bytes audio_data = 2;
*/
value: Uint8Array;
case: "audioData";
} | { case: undefined; value?: undefined };
};
/**
* Describes the message ito.TranscribeStreamRequest.
* Use `create(TranscribeStreamRequestSchema)` to create a new message.
*/
export const TranscribeStreamRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 5);
/**
* The response message containing the final transcript.
*
* @generated from message ito.TranscriptionResponse
*/
export type TranscriptionResponse = Message<"ito.TranscriptionResponse"> & {
/**
* @generated from field: string transcript = 1;
*/
transcript: string;
/**
* @generated from field: ito.ClientError error = 2;
*/
error?: ClientError;
};
/**
* Describes the message ito.TranscriptionResponse.
* Use `create(TranscriptionResponseSchema)` to create a new message.
*/
export const TranscriptionResponseSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 6);
/**
* Notes
* -----------------------------------------------------------------
*
* @generated from message ito.Note
*/
export type Note = Message<"ito.Note"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string user_id = 2;
*/
userId: string;
/**
* @generated from field: string interaction_id = 3;
*/
interactionId: string;
/**
* @generated from field: string content = 4;
*/
content: string;
/**
* @generated from field: string created_at = 5;
*/
createdAt: string;
/**
* @generated from field: string updated_at = 6;
*/
updatedAt: string;
/**
* @generated from field: string deleted_at = 7;
*/
deletedAt: string;
};
/**
* Describes the message ito.Note.
* Use `create(NoteSchema)` to create a new message.
*/
export const NoteSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 7);
/**
* @generated from message ito.CreateNoteRequest
*/
export type CreateNoteRequest = Message<"ito.CreateNoteRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string interaction_id = 2;
*/
interactionId: string;
/**
* @generated from field: string content = 3;
*/
content: string;
};
/**
* Describes the message ito.CreateNoteRequest.
* Use `create(CreateNoteRequestSchema)` to create a new message.
*/
export const CreateNoteRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 8);
/**
* @generated from message ito.GetNoteRequest
*/
export type GetNoteRequest = Message<"ito.GetNoteRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
};
/**
* Describes the message ito.GetNoteRequest.
* Use `create(GetNoteRequestSchema)` to create a new message.
*/
export const GetNoteRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 9);
/**
* @generated from message ito.ListNotesRequest
*/
export type ListNotesRequest = Message<"ito.ListNotesRequest"> & {
/**
* Optional. ISO 8601 format. If not provided, fetch all.
*
* @generated from field: string since_timestamp = 1;
*/
sinceTimestamp: string;
};
/**
* Describes the message ito.ListNotesRequest.
* Use `create(ListNotesRequestSchema)` to create a new message.
*/
export const ListNotesRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 10);
/**
* @generated from message ito.ListNotesResponse
*/
export type ListNotesResponse = Message<"ito.ListNotesResponse"> & {
/**
* @generated from field: repeated ito.Note notes = 1;
*/
notes: Note[];
};
/**
* Describes the message ito.ListNotesResponse.
* Use `create(ListNotesResponseSchema)` to create a new message.
*/
export const ListNotesResponseSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 11);
/**
* @generated from message ito.UpdateNoteRequest
*/
export type UpdateNoteRequest = Message<"ito.UpdateNoteRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string content = 2;
*/
content: string;
};
/**
* Describes the message ito.UpdateNoteRequest.
* Use `create(UpdateNoteRequestSchema)` to create a new message.
*/
export const UpdateNoteRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 12);
/**
* @generated from message ito.DeleteNoteRequest
*/
export type DeleteNoteRequest = Message<"ito.DeleteNoteRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
};
/**
* Describes the message ito.DeleteNoteRequest.
* Use `create(DeleteNoteRequestSchema)` to create a new message.
*/
export const DeleteNoteRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 13);
/**
* Interactions
* -----------------------------------------------------------------
*
* @generated from message ito.Interaction
*/
export type Interaction = Message<"ito.Interaction"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string user_id = 2;
*/
userId: string;
/**
* @generated from field: string title = 3;
*/
title: string;
/**
* JSON string
*
* @generated from field: string asr_output = 4;
*/
asrOutput: string;
/**
* JSON string
*
* @generated from field: string llm_output = 5;
*/
llmOutput: string;
/**
* 100 MB limit
*
* @generated from field: bytes raw_audio = 6;
*/
rawAudio: Uint8Array;
/**
* Duration in milliseconds
*
* @generated from field: int32 duration_ms = 7;
*/
durationMs: number;
/**
* @generated from field: string created_at = 8;
*/
createdAt: string;
/**
* @generated from field: string updated_at = 9;
*/
updatedAt: string;
/**
* @generated from field: string deleted_at = 10;
*/
deletedAt: string;
/**
* UUID reference to S3 stored audio
*
* @generated from field: optional string raw_audio_id = 11;
*/
rawAudioId?: string;
};
/**
* Describes the message ito.Interaction.
* Use `create(InteractionSchema)` to create a new message.
*/
export const InteractionSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 14);
/**
* @generated from message ito.CreateInteractionRequest
*/
export type CreateInteractionRequest = Message<"ito.CreateInteractionRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string title = 2;
*/
title: string;
/**
* @generated from field: string asr_output = 3;
*/
asrOutput: string;
/**
* @generated from field: string llm_output = 4;
*/
llmOutput: string;
/**
* 100 MB limit
*
* @generated from field: bytes raw_audio = 5;
*/
rawAudio: Uint8Array;
/**
* Duration in milliseconds
*
* @generated from field: int32 duration_ms = 6;
*/
durationMs: number;
};
/**
* Describes the message ito.CreateInteractionRequest.
* Use `create(CreateInteractionRequestSchema)` to create a new message.
*/
export const CreateInteractionRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 15);
/**
* @generated from message ito.GetInteractionRequest
*/
export type GetInteractionRequest = Message<"ito.GetInteractionRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
};
/**
* Describes the message ito.GetInteractionRequest.
* Use `create(GetInteractionRequestSchema)` to create a new message.
*/
export const GetInteractionRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 16);
/**
* @generated from message ito.ListInteractionsRequest
*/
export type ListInteractionsRequest = Message<"ito.ListInteractionsRequest"> & {
/**
* Optional. ISO 8601 format. If not provided, fetch all.
*
* @generated from field: string since_timestamp = 1;
*/
sinceTimestamp: string;
};
/**
* Describes the message ito.ListInteractionsRequest.
* Use `create(ListInteractionsRequestSchema)` to create a new message.
*/
export const ListInteractionsRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 17);
/**
* @generated from message ito.ListInteractionsResponse
*/
export type ListInteractionsResponse = Message<"ito.ListInteractionsResponse"> & {
/**
* @generated from field: repeated ito.Interaction interactions = 1;
*/
interactions: Interaction[];
};
/**
* Describes the message ito.ListInteractionsResponse.
* Use `create(ListInteractionsResponseSchema)` to create a new message.
*/
export const ListInteractionsResponseSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 18);
/**
* @generated from message ito.UpdateInteractionRequest
*/
export type UpdateInteractionRequest = Message<"ito.UpdateInteractionRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string title = 2;
*/
title: string;
};
/**
* Describes the message ito.UpdateInteractionRequest.
* Use `create(UpdateInteractionRequestSchema)` to create a new message.
*/
export const UpdateInteractionRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 19);
/**
* @generated from message ito.DeleteInteractionRequest
*/
export type DeleteInteractionRequest = Message<"ito.DeleteInteractionRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
};
/**
* Describes the message ito.DeleteInteractionRequest.
* Use `create(DeleteInteractionRequestSchema)` to create a new message.
*/
export const DeleteInteractionRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 20);
/**
* Dictionary
* -----------------------------------------------------------------
*
* @generated from message ito.DictionaryItem
*/
export type DictionaryItem = Message<"ito.DictionaryItem"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string user_id = 2;
*/
userId: string;
/**
* @generated from field: string word = 3;
*/
word: string;
/**
* @generated from field: string pronunciation = 4;
*/
pronunciation: string;
/**
* @generated from field: string created_at = 5;
*/
createdAt: string;
/**
* @generated from field: string updated_at = 6;
*/
updatedAt: string;
/**
* @generated from field: string deleted_at = 7;
*/
deletedAt: string;
};
/**
* Describes the message ito.DictionaryItem.
* Use `create(DictionaryItemSchema)` to create a new message.
*/
export const DictionaryItemSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 21);
/**
* @generated from message ito.CreateDictionaryItemRequest
*/
export type CreateDictionaryItemRequest = Message<"ito.CreateDictionaryItemRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string word = 2;
*/
word: string;
/**
* @generated from field: string pronunciation = 3;
*/
pronunciation: string;
};
/**
* Describes the message ito.CreateDictionaryItemRequest.
* Use `create(CreateDictionaryItemRequestSchema)` to create a new message.
*/
export const CreateDictionaryItemRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 22);
/**
* @generated from message ito.ListDictionaryItemsRequest
*/
export type ListDictionaryItemsRequest = Message<"ito.ListDictionaryItemsRequest"> & {
/**
* Optional. ISO 8601 format. If not provided, fetch all.
*
* @generated from field: string since_timestamp = 1;
*/
sinceTimestamp: string;
};
/**
* Describes the message ito.ListDictionaryItemsRequest.
* Use `create(ListDictionaryItemsRequestSchema)` to create a new message.
*/
export const ListDictionaryItemsRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 23);
/**
* @generated from message ito.ListDictionaryItemsResponse
*/
export type ListDictionaryItemsResponse = Message<"ito.ListDictionaryItemsResponse"> & {
/**
* @generated from field: repeated ito.DictionaryItem items = 1;
*/
items: DictionaryItem[];
};
/**
* Describes the message ito.ListDictionaryItemsResponse.
* Use `create(ListDictionaryItemsResponseSchema)` to create a new message.
*/
export const ListDictionaryItemsResponseSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 24);
/**
* @generated from message ito.UpdateDictionaryItemRequest
*/
export type UpdateDictionaryItemRequest = Message<"ito.UpdateDictionaryItemRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string word = 2;
*/
word: string;
/**
* @generated from field: string pronunciation = 3;
*/
pronunciation: string;
};
/**
* Describes the message ito.UpdateDictionaryItemRequest.
* Use `create(UpdateDictionaryItemRequestSchema)` to create a new message.
*/
export const UpdateDictionaryItemRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 25);
/**
* @generated from message ito.DeleteDictionaryItemRequest
*/
export type DeleteDictionaryItemRequest = Message<"ito.DeleteDictionaryItemRequest"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
};
/**
* Describes the message ito.DeleteDictionaryItemRequest.
* Use `create(DeleteDictionaryItemRequestSchema)` to create a new message.
*/
export const DeleteDictionaryItemRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 26);
/**
* User Data
* -----------------------------------------------------------------
*
* Empty - user_id will be extracted from the authenticated user's token
*
* @generated from message ito.DeleteUserDataRequest
*/
export type DeleteUserDataRequest = Message<"ito.DeleteUserDataRequest"> & {
};
/**
* Describes the message ito.DeleteUserDataRequest.
* Use `create(DeleteUserDataRequestSchema)` to create a new message.
*/
export const DeleteUserDataRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 27);
/**
* @generated from message ito.LlmSettings
*/
export type LlmSettings = Message<"ito.LlmSettings"> & {
/**
* @generated from field: optional string asr_model = 1;
*/
asrModel?: string;
/**
* @generated from field: optional string asr_provider = 2;
*/
asrProvider?: string;
/**
* @generated from field: optional string asr_prompt = 3;
*/
asrPrompt?: string;
/**
* @generated from field: optional string llm_provider = 4;
*/
llmProvider?: string;
/**
* @generated from field: optional string llm_model = 5;
*/
llmModel?: string;
/**
* @generated from field: optional float llm_temperature = 6;
*/
llmTemperature?: number;
/**
* @generated from field: optional string transcription_prompt = 7;
*/
transcriptionPrompt?: string;
/**
* @generated from field: optional string editing_prompt = 8;
*/
editingPrompt?: string;
/**
* @generated from field: optional float no_speech_threshold = 9;
*/
noSpeechThreshold?: number;
/**
* @generated from field: optional float low_quality_threshold = 10;
*/
lowQualityThreshold?: number;
};
/**
* Describes the message ito.LlmSettings.
* Use `create(LlmSettingsSchema)` to create a new message.
*/
export const LlmSettingsSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 28);
/**
* @generated from message ito.AdvancedSettings
*/
export type AdvancedSettings = Message<"ito.AdvancedSettings"> & {
/**
* @generated from field: string id = 1;
*/
id: string;
/**
* @generated from field: string user_id = 2;
*/
userId: string;
/**
* @generated from field: string created_at = 3;
*/
createdAt: string;
/**
* @generated from field: string updated_at = 4;
*/
updatedAt: string;
/**
* @generated from field: ito.LlmSettings llm = 5;
*/
llm?: LlmSettings;
/**
* @generated from field: ito.LlmSettings default = 6;
*/
default?: LlmSettings;
};
/**
* Describes the message ito.AdvancedSettings.
* Use `create(AdvancedSettingsSchema)` to create a new message.
*/
export const AdvancedSettingsSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 29);
/**
* Empty - user_id will be extracted from the authenticated user's token
*
* @generated from message ito.GetAdvancedSettingsRequest
*/
export type GetAdvancedSettingsRequest = Message<"ito.GetAdvancedSettingsRequest"> & {
};
/**
* Describes the message ito.GetAdvancedSettingsRequest.
* Use `create(GetAdvancedSettingsRequestSchema)` to create a new message.
*/
export const GetAdvancedSettingsRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 30);
/**
* @generated from message ito.UpdateAdvancedSettingsRequest
*/
export type UpdateAdvancedSettingsRequest = Message<"ito.UpdateAdvancedSettingsRequest"> & {
/**
* @generated from field: ito.LlmSettings llm = 1;
*/
llm?: LlmSettings;
};
/**
* Describes the message ito.UpdateAdvancedSettingsRequest.
* Use `create(UpdateAdvancedSettingsRequestSchema)` to create a new message.
*/
export const UpdateAdvancedSettingsRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 31);
/**
* Timing Analytics
* -----------------------------------------------------------------
*
* @generated from message ito.TimingEvent
*/
export type TimingEvent = Message<"ito.TimingEvent"> & {
/**
* @generated from field: string name = 1;
*/
name: string;
/**
* @generated from field: double start_ms = 2;
*/
startMs: number;
/**
* @generated from field: optional double end_ms = 3;
*/
endMs?: number;
/**
* @generated from field: optional double duration_ms = 4;
*/
durationMs?: number;
};
/**
* Describes the message ito.TimingEvent.
* Use `create(TimingEventSchema)` to create a new message.
*/
export const TimingEventSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 32);
/**
* @generated from message ito.TimingReport
*/
export type TimingReport = Message<"ito.TimingReport"> & {
/**
* @generated from field: string interaction_id = 1;
*/
interactionId: string;
/**
* @generated from field: string user_id = 2;
*/
userId: string;
/**
* @generated from field: string platform = 3;
*/
platform: string;
/**
* @generated from field: string app_version = 4;
*/
appVersion: string;
/**
* @generated from field: string hostname = 5;
*/
hostname: string;
/**
* @generated from field: string architecture = 6;
*/
architecture: string;
/**
* @generated from field: string timestamp = 7;
*/
timestamp: string;
/**
* @generated from field: repeated ito.TimingEvent events = 8;
*/
events: TimingEvent[];
/**
* @generated from field: double total_duration_ms = 9;
*/
totalDurationMs: number;
};
/**
* Describes the message ito.TimingReport.
* Use `create(TimingReportSchema)` to create a new message.
*/
export const TimingReportSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 33);
/**
* @generated from message ito.SubmitTimingReportsRequest
*/
export type SubmitTimingReportsRequest = Message<"ito.SubmitTimingReportsRequest"> & {
/**
* @generated from field: repeated ito.TimingReport reports = 1;
*/
reports: TimingReport[];
};
/**
* Describes the message ito.SubmitTimingReportsRequest.
* Use `create(SubmitTimingReportsRequestSchema)` to create a new message.
*/
export const SubmitTimingReportsRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 34);
/**
* Empty response
*
* @generated from message ito.SubmitTimingReportsResponse
*/
export type SubmitTimingReportsResponse = Message<"ito.SubmitTimingReportsResponse"> & {
};
/**
* Describes the message ito.SubmitTimingReportsResponse.
* Use `create(SubmitTimingReportsResponseSchema)` to create a new message.
*/
export const SubmitTimingReportsResponseSchema: GenMessage = /*@__PURE__*/
messageDesc(file_ito, 35);
/**
* @generated from enum ito.ItoMode
*/
export enum ItoMode {
/**
* @generated from enum value: TRANSCRIBE = 0;
*/
TRANSCRIBE = 0,
/**
* @generated from enum value: EDIT = 1;
*/
EDIT = 1,
}
/**
* Describes the enum ito.ItoMode.
*/
export const ItoModeSchema: GenEnum = /*@__PURE__*/
enumDesc(file_ito, 0);
/**
* Error Types
* -----------------------------------------------------------------
*
* @generated from enum ito.ClientProvider
*/
export enum ClientProvider {
/**
* @generated from enum value: GROQ = 0;
*/
GROQ = 0,
/**
* @generated from enum value: CEREBRAS = 1;
*/
CEREBRAS = 1,
}
/**
* Describes the enum ito.ClientProvider.
*/
export const ClientProviderSchema: GenEnum = /*@__PURE__*/
enumDesc(file_ito, 1);
/**
* @generated from enum ito.ErrorType
*/
export enum ErrorType {
/**
* @generated from enum value: CONFIGURATION = 0;
*/
CONFIGURATION = 0,
/**
* @generated from enum value: AVAILABILITY = 1;
*/
AVAILABILITY = 1,
/**
* @generated from enum value: AUDIO = 2;
*/
AUDIO = 2,
/**
* @generated from enum value: API = 3;
*/
API = 3,
}
/**
* Describes the enum ito.ErrorType.
*/
export const ErrorTypeSchema: GenEnum = /*@__PURE__*/
enumDesc(file_ito, 2);
/**
* @generated from service ito.ItoService
*/
export const ItoService: GenService<{
/**
* Streams audio chunks from the client and gets a single response.
* This is the ideal method for dictation to reduce latency and memory usage.
*
* @generated from rpc ito.ItoService.TranscribeStream
*/
transcribeStream: {
methodKind: "client_streaming";
input: typeof AudioChunkSchema;
output: typeof TranscriptionResponseSchema;
},
/**
* Enhanced streaming transcription that accepts configuration data in-stream.
* Config can be sent before, during, or omitted entirely. Multiple config messages
* are merged by the server. This allows immediate streaming without waiting for context.
*
* @generated from rpc ito.ItoService.TranscribeStreamV2
*/
transcribeStreamV2: {
methodKind: "client_streaming";
input: typeof TranscribeStreamRequestSchema;
output: typeof TranscriptionResponseSchema;
},
/**
* Note Service
*
* @generated from rpc ito.ItoService.CreateNote
*/
createNote: {
methodKind: "unary";
input: typeof CreateNoteRequestSchema;
output: typeof NoteSchema;
},
/**
* @generated from rpc ito.ItoService.GetNote
*/
getNote: {
methodKind: "unary";
input: typeof GetNoteRequestSchema;
output: typeof NoteSchema;
},
/**
* @generated from rpc ito.ItoService.ListNotes
*/
listNotes: {
methodKind: "unary";
input: typeof ListNotesRequestSchema;
output: typeof ListNotesResponseSchema;
},
/**
* @generated from rpc ito.ItoService.UpdateNote
*/
updateNote: {
methodKind: "unary";
input: typeof UpdateNoteRequestSchema;
output: typeof NoteSchema;
},
/**
* @generated from rpc ito.ItoService.DeleteNote
*/
deleteNote: {
methodKind: "unary";
input: typeof DeleteNoteRequestSchema;
output: typeof EmptySchema;
},
/**
* Interaction Service
*
* @generated from rpc ito.ItoService.CreateInteraction
*/
createInteraction: {
methodKind: "unary";
input: typeof CreateInteractionRequestSchema;
output: typeof InteractionSchema;
},
/**
* @generated from rpc ito.ItoService.GetInteraction
*/
getInteraction: {
methodKind: "unary";
input: typeof GetInteractionRequestSchema;
output: typeof InteractionSchema;
},
/**
* @generated from rpc ito.ItoService.ListInteractions
*/
listInteractions: {
methodKind: "unary";
input: typeof ListInteractionsRequestSchema;
output: typeof ListInteractionsResponseSchema;
},
/**
* @generated from rpc ito.ItoService.UpdateInteraction
*/
updateInteraction: {
methodKind: "unary";
input: typeof UpdateInteractionRequestSchema;
output: typeof InteractionSchema;
},
/**
* @generated from rpc ito.ItoService.DeleteInteraction
*/
deleteInteraction: {
methodKind: "unary";
input: typeof DeleteInteractionRequestSchema;
output: typeof EmptySchema;
},
/**
* Dictionary Service
*
* @generated from rpc ito.ItoService.CreateDictionaryItem
*/
createDictionaryItem: {
methodKind: "unary";
input: typeof CreateDictionaryItemRequestSchema;
output: typeof DictionaryItemSchema;
},
/**
* @generated from rpc ito.ItoService.ListDictionaryItems
*/
listDictionaryItems: {
methodKind: "unary";
input: typeof ListDictionaryItemsRequestSchema;
output: typeof ListDictionaryItemsResponseSchema;
},
/**
* @generated from rpc ito.ItoService.UpdateDictionaryItem
*/
updateDictionaryItem: {
methodKind: "unary";
input: typeof UpdateDictionaryItemRequestSchema;
output: typeof DictionaryItemSchema;
},
/**
* @generated from rpc ito.ItoService.DeleteDictionaryItem
*/
deleteDictionaryItem: {
methodKind: "unary";
input: typeof DeleteDictionaryItemRequestSchema;
output: typeof EmptySchema;
},
/**
* User Data Service
*
* @generated from rpc ito.ItoService.DeleteUserData
*/
deleteUserData: {
methodKind: "unary";
input: typeof DeleteUserDataRequestSchema;
output: typeof EmptySchema;
},
/**
* Advanced Settings Service
*
* @generated from rpc ito.ItoService.GetAdvancedSettings
*/
getAdvancedSettings: {
methodKind: "unary";
input: typeof GetAdvancedSettingsRequestSchema;
output: typeof AdvancedSettingsSchema;
},
/**
* @generated from rpc ito.ItoService.UpdateAdvancedSettings
*/
updateAdvancedSettings: {
methodKind: "unary";
input: typeof UpdateAdvancedSettingsRequestSchema;
output: typeof AdvancedSettingsSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_ito, 0);
/**
* @generated from service ito.TimingService
*/
export const TimingService: GenService<{
/**
* Submit timing reports for interaction analytics
*
* @generated from rpc ito.TimingService.SubmitTimingReports
*/
submitTimingReports: {
methodKind: "unary";
input: typeof SubmitTimingReportsRequestSchema;
output: typeof SubmitTimingReportsResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_ito, 1);
================================================
FILE: app/hooks/useBillingState.test.ts
================================================
import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'
import React from 'react'
import { createRoot, Root } from 'react-dom/client'
import { act } from 'react'
import { Window } from 'happy-dom'
let window: Window
let document: any
let mockAddEventListener: ReturnType
let mockRemoveEventListener: ReturnType
const mockBillingApi = {
status: mock(),
}
const mockTrialApi = {
complete: mock(),
}
const mockApi = {
billing: mockBillingApi,
trial: mockTrialApi,
send: mock(),
}
const mockElectronStore = {
get: mock((key: string) => {
if (key === 'auth') {
return mockStoreData.auth
}
return {}
}),
set: mock(),
}
const mockStoreData: { auth: { billing?: any } } = {
auth: {},
}
const originalWindow = globalThis.window
beforeEach(() => {
window = new Window()
document = window.document
global.window = window as any
global.document = document as any
// Reset mock state
mockStoreData.auth = {}
mockBillingApi.status.mockClear()
mockTrialApi.complete.mockClear()
mockApi.send.mockClear()
mockElectronStore.get.mockClear()
// Create fresh mocks for event listeners
mockAddEventListener = mock((event: string, handler: () => void) => {})
mockRemoveEventListener = mock((event: string, handler: () => void) => {})
// Setup window mocks with addEventListener/removeEventListener
globalThis.window = {
...window,
addEventListener: mockAddEventListener as any,
removeEventListener: mockRemoveEventListener as any,
api: mockApi as any,
electron: {
store: mockElectronStore as any,
},
} as any
})
afterEach(() => {
globalThis.window = originalWindow
})
// Simple test utility to render a hook
function renderHook(hook: () => T): {
result: { current: T }
rerender: () => void
unmount: () => void
waitFor: (fn: () => boolean, timeout?: number) => Promise
} {
const result: { current: T } = { current: null as any }
let root: Root | null = null
let container: any = null
const TestComponent = () => {
const hookResult = hook()
result.current = hookResult
return null
}
const mount = () => {
container = document.createElement('div')
root = createRoot(container)
act(() => {
root!.render(React.createElement(TestComponent))
})
// Wait for initial render to complete
return new Promise(resolve => {
setTimeout(() => resolve(), 0)
})
}
// Mount synchronously wrapped in act
mount()
const rerender = () => {
if (root && container) {
act(() => {
root!.render(React.createElement(TestComponent))
})
}
}
const unmount = () => {
if (root) {
act(() => {
root?.unmount()
})
root = null
}
if (container && container.parentNode) {
container.parentNode.removeChild(container)
}
container = null
}
const waitFor = async (fn: () => boolean, timeout = 1000): Promise => {
const start = Date.now()
while (Date.now() - start < timeout) {
// Ensure result.current is not null before checking
if (result.current !== null && fn()) {
return
}
await new Promise(resolve => setTimeout(resolve, 10))
}
throw new Error('waitFor timeout')
}
return { result, rerender, unmount, waitFor }
}
import { useBillingState } from './useBillingState'
describe('useBillingState', () => {
it('initializes with loading state and no cached data', async () => {
mockBillingApi.status.mockResolvedValue({
success: true,
pro_status: 'none' as const,
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: false,
},
})
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(result.current.isLoading).toBe(false)
expect(result.current.error).toBe(null)
expect(result.current.proStatus).toBe('none')
expect(result.current.isPro).toBe(false)
expect(mockBillingApi.status).toHaveBeenCalledTimes(1)
})
it('loads cached billing state from electron store', async () => {
const cachedState = {
proStatus: 'free_trial' as const,
subscriptionStartAt: '2024-01-01T00:00:00.000Z',
trialDays: 14,
trialStartAt: '2024-01-01T00:00:00.000Z',
daysLeft: 10,
isTrialActive: true,
hasCompletedTrial: false,
}
mockStoreData.auth.billing = cachedState
// Mock API response to match cached state (refresh() will be called after mount)
mockBillingApi.status.mockResolvedValue({
success: true,
pro_status: 'free_trial' as const,
trial: {
trialDays: 14,
trialStartAt: '2024-01-01T00:00:00.000Z',
daysLeft: 10,
isTrialActive: true,
hasCompletedTrial: false,
},
})
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(result.current.proStatus).toBe('free_trial')
expect(result.current.isTrialActive).toBe(true)
expect(result.current.daysLeft).toBe(10)
})
it('handles successful billing status fetch', async () => {
const mockResponse = {
success: true,
pro_status: 'active_pro' as const,
subscriptionStartAt: '2024-01-01T00:00:00.000Z',
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: true,
},
}
mockBillingApi.status.mockResolvedValue(mockResponse)
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(result.current.proStatus).toBe('active_pro')
expect(result.current.isPro).toBe(true)
expect(result.current.hasSubscription).toBe(true)
expect(result.current.error).toBe(null)
expect(result.current.subscriptionStartAt).toBeInstanceOf(Date)
expect(mockApi.send).toHaveBeenCalledWith(
'electron-store-set',
'auth.billing',
expect.objectContaining({
proStatus: 'active_pro',
}),
)
})
it('handles billing status fetch error', async () => {
mockBillingApi.status.mockResolvedValue({
success: false,
error: 'API error',
})
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(result.current.error).toBe('API error')
expect(result.current.proStatus).toBe('none')
})
it('handles billing status fetch exception', async () => {
const error = new Error('Network error')
mockBillingApi.status.mockRejectedValue(error)
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(result.current.error).toBe('Network error')
expect(result.current.proStatus).toBe('none')
})
it('refresh function updates billing state', async () => {
const initialResponse = {
success: true,
pro_status: 'none' as const,
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: false,
},
}
const updatedResponse = {
success: true,
pro_status: 'active_pro' as const,
subscriptionStartAt: '2024-01-01T00:00:00.000Z',
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: true,
},
}
mockBillingApi.status.mockResolvedValueOnce(initialResponse)
mockBillingApi.status.mockResolvedValueOnce(updatedResponse)
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(result.current.proStatus).toBe('none')
await result.current.refresh()
await waitFor(() => result.current.proStatus === 'active_pro')
expect(result.current.proStatus).toBe('active_pro')
expect(result.current.isPro).toBe(true)
expect(mockBillingApi.status).toHaveBeenCalledTimes(2)
})
it('completeTrial function calls trial.complete and refreshes', async () => {
const billingResponse = {
success: true,
pro_status: 'none' as const,
trial: {
trialDays: 14,
trialStartAt: '2024-01-01T00:00:00.000Z',
daysLeft: 5,
isTrialActive: true,
hasCompletedTrial: false,
},
}
const trialCompleteResponse = {
success: true,
trialDays: 14,
trialStartAt: '2024-01-01T00:00:00.000Z',
daysLeft: 5,
isTrialActive: false,
hasCompletedTrial: true,
}
mockBillingApi.status.mockResolvedValue(billingResponse)
mockTrialApi.complete.mockResolvedValue(trialCompleteResponse)
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
mockBillingApi.status.mockClear()
await result.current.completeTrial()
await waitFor(() => !result.current.isLoading)
expect(mockTrialApi.complete).toHaveBeenCalledTimes(1)
expect(mockBillingApi.status).toHaveBeenCalledTimes(1)
})
it('completeTrial handles errors gracefully', async () => {
const billingResponse = {
success: true,
pro_status: 'none' as const,
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: false,
},
}
mockBillingApi.status.mockResolvedValue(billingResponse)
mockTrialApi.complete.mockResolvedValue({
success: false,
error: 'Trial completion failed',
})
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
// Clear the mock so refresh() won't be called after completeTrial
mockBillingApi.status.mockClear()
await result.current.completeTrial()
await waitFor(
() => !result.current.isLoading && result.current.error !== null,
)
expect(result.current.error).toBe('Trial completion failed')
})
it('handles missing trial data gracefully', async () => {
const mockResponse = {
success: true,
pro_status: 'none' as const,
trial: undefined,
}
mockBillingApi.status.mockResolvedValue(mockResponse)
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(result.current.proStatus).toBe('none')
expect(result.current.trialDays).toBe(14)
expect(result.current.daysLeft).toBe(0)
expect(result.current.isTrialActive).toBe(false)
})
it('converts date strings to Date objects correctly', async () => {
const mockResponse = {
success: true,
pro_status: 'active_pro' as const,
subscriptionStartAt: '2024-01-15T10:30:00.000Z',
trial: {
trialDays: 14,
trialStartAt: '2024-01-01T00:00:00.000Z',
daysLeft: 5,
isTrialActive: false,
hasCompletedTrial: true,
},
}
mockBillingApi.status.mockResolvedValue(mockResponse)
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(result.current.subscriptionStartAt).toBeInstanceOf(Date)
expect(result.current.subscriptionStartAt?.toISOString()).toBe(
'2024-01-15T10:30:00.000Z',
)
expect(result.current.trialStartAt).toBeInstanceOf(Date)
expect(result.current.trialStartAt?.toISOString()).toBe(
'2024-01-01T00:00:00.000Z',
)
})
it('handles cache read errors gracefully', async () => {
mockElectronStore.get.mockImplementation(() => {
throw new Error('Store read error')
})
mockBillingApi.status.mockResolvedValue({
success: true,
pro_status: 'none' as const,
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: false,
},
})
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(result.current.proStatus).toBe('none')
expect(mockBillingApi.status).toHaveBeenCalledTimes(1)
})
it('handles cache write errors gracefully', async () => {
mockApi.send.mockImplementation(() => {
throw new Error('Store write error')
})
const mockResponse = {
success: true,
pro_status: 'active_pro' as const,
subscriptionStartAt: '2024-01-01T00:00:00.000Z',
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: true,
},
}
mockBillingApi.status.mockResolvedValue(mockResponse)
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(result.current.proStatus).toBe('active_pro')
expect(result.current.error).toBe(null)
})
it('sets up window focus listener on mount', async () => {
mockBillingApi.status.mockResolvedValue({
success: true,
pro_status: 'none' as const,
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: false,
},
})
const { result, waitFor, unmount } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(mockAddEventListener).toHaveBeenCalledWith(
'focus',
expect.any(Function),
)
unmount()
expect(mockRemoveEventListener).toHaveBeenCalledWith(
'focus',
expect.any(Function),
)
})
it('refreshes billing state when window gains focus', async () => {
mockBillingApi.status.mockResolvedValue({
success: true,
pro_status: 'none' as const,
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: false,
},
})
const { result, waitFor } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
mockBillingApi.status.mockClear()
const focusHandler = mockAddEventListener.mock.calls.find(
call => call[0] === 'focus',
)?.[1] as () => void
expect(focusHandler).toBeDefined()
if (focusHandler) {
await focusHandler()
await waitFor(() => mockBillingApi.status.mock.calls.length > 0)
expect(mockBillingApi.status).toHaveBeenCalledTimes(1)
}
})
it('sets up periodic refresh interval', async () => {
const originalSetInterval = global.setInterval
const mockSetInterval = mock(() => ({}) as any)
global.setInterval = mockSetInterval as any
mockBillingApi.status.mockResolvedValue({
success: true,
pro_status: 'none' as const,
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: false,
},
})
const { result, waitFor, unmount } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
expect(mockSetInterval).toHaveBeenCalledWith(
expect.any(Function),
2 * 60 * 1000,
)
unmount()
global.setInterval = originalSetInterval
})
it('periodic refresh calls refresh function', async () => {
const originalSetInterval = global.setInterval
const originalClearInterval = global.clearInterval
let intervalCallback: (() => void) | null = null
const intervalId: any = {}
const mockSetInterval = mock((callback: () => void, delay: number) => {
intervalCallback = callback
return intervalId
})
const mockClearInterval = mock(() => {})
global.setInterval = mockSetInterval as any
global.clearInterval = mockClearInterval as any
mockBillingApi.status.mockResolvedValue({
success: true,
pro_status: 'none' as const,
trial: {
trialDays: 14,
trialStartAt: null,
daysLeft: 0,
isTrialActive: false,
hasCompletedTrial: false,
},
})
const { result, waitFor, unmount } = renderHook(() => useBillingState())
await waitFor(() => !result.current.isLoading)
mockBillingApi.status.mockClear()
if (intervalCallback) {
await (intervalCallback as () => void)()
await waitFor(() => mockBillingApi.status.mock.calls.length > 0)
expect(mockBillingApi.status).toHaveBeenCalledTimes(1)
}
unmount()
expect(mockClearInterval).toHaveBeenCalledWith(intervalId)
global.setInterval = originalSetInterval
global.clearInterval = originalClearInterval
})
})
================================================
FILE: app/hooks/useBillingState.ts
================================================
import { useCallback, useEffect, useMemo, useState } from 'react'
export type BillingState = {
proStatus: 'active_pro' | 'free_trial' | 'none'
subscriptionStartAt: Date | null
subscriptionEndAt: Date | null
isScheduledForCancellation: boolean
trialDays: number
trialStartAt: Date | null
daysLeft: number
isTrialActive: boolean
hasCompletedTrial: boolean
}
export function useBillingState() {
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
const [state, setState] = useState(null)
useEffect(() => {
try {
const authStore = window.electron?.store?.get('auth') || {}
const cached: (BillingState & { fetchedAt?: string }) | undefined =
authStore?.billing
if (cached) {
setState({
...cached,
subscriptionStartAt: cached.subscriptionStartAt
? new Date(cached.subscriptionStartAt)
: null,
subscriptionEndAt: cached.subscriptionEndAt
? new Date(cached.subscriptionEndAt)
: null,
trialStartAt: cached.trialStartAt
? new Date(cached.trialStartAt)
: null,
})
}
} catch {
console.warn('Failed to load billing state from cache')
} finally {
setIsLoading(false)
}
}, [])
const cacheState = useCallback((s: BillingState) => {
try {
const withFetchedAt = { ...s, fetchedAt: new Date().toISOString() }
window.api.send('electron-store-set', 'auth.billing', withFetchedAt)
} catch {
console.warn('Failed to cache billing state')
}
}, [])
const refresh = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const res = await window.api.billing.status()
if (!res?.success) {
setError(res?.error || 'Failed to load billing status')
} else {
const subStart = res?.subscriptionStartAt
? new Date(res.subscriptionStartAt)
: null
const subEnd = res?.subscriptionEndAt
? new Date(res.subscriptionEndAt)
: null
const trial = res?.trial || {}
const next: BillingState = {
proStatus: res.pro_status,
subscriptionStartAt: subStart,
subscriptionEndAt: subEnd,
isScheduledForCancellation: !!res?.isScheduledForCancellation,
trialDays: trial.trialDays ?? 14,
trialStartAt: trial.trialStartAt
? new Date(trial.trialStartAt)
: null,
daysLeft: trial.daysLeft ?? 0,
isTrialActive: !!trial.isTrialActive,
hasCompletedTrial: !!trial.hasCompletedTrial,
}
setState(next)
cacheState(next)
}
} catch (e: any) {
setError(e?.message || 'Failed to load billing status')
} finally {
setIsLoading(false)
}
}, [cacheState])
useEffect(() => {
refresh()
}, [refresh])
// Periodically refresh billing state to stay in sync with webhook updates
useEffect(() => {
const interval = setInterval(
() => {
refresh()
},
2 * 60 * 1000,
) // Refresh every 2 minutes
return () => clearInterval(interval)
}, [refresh])
// Refresh billing state when window regains focus
useEffect(() => {
const handleFocus = () => {
refresh()
}
window.addEventListener('focus', handleFocus)
return () => {
window.removeEventListener('focus', handleFocus)
}
}, [refresh])
const completeTrial = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const res = await window.api.trial.complete()
if (!res?.success) {
setError(res?.error || 'Failed to complete trial')
} else {
await refresh()
}
} catch (e: any) {
setError(e?.message || 'Failed to complete trial')
} finally {
setIsLoading(false)
}
}, [refresh])
const api = useMemo(
() => ({
isLoading,
error,
proStatus: state?.proStatus ?? 'none',
isPro: (state?.proStatus ?? 'none') === 'active_pro',
hasSubscription: (state?.proStatus ?? 'none') === 'active_pro',
subscriptionStartAt: state?.subscriptionStartAt ?? null,
subscriptionEndAt: state?.subscriptionEndAt ?? null,
isScheduledForCancellation: state?.isScheduledForCancellation ?? false,
isTrialActive: !!state?.isTrialActive,
daysLeft: state?.daysLeft ?? 0,
trialDays: state?.trialDays ?? 14,
trialStartAt: state?.trialStartAt ?? null,
hasCompletedTrial: !!state?.hasCompletedTrial,
refresh,
completeTrial,
}),
[isLoading, error, state, refresh, completeTrial],
)
return api
}
export default useBillingState
================================================
FILE: app/hooks/useDeviceChangeListener.ts
================================================
import { useEffect } from 'react'
import log from 'electron-log'
/**
* A React hook that listens for changes in media devices (e.g., plugging in or
* unplugging a microphone/headset) and notifies the main process.
* This should be used once in a long-lived component, like the root App component.
*/
export const useDeviceChangeListener = (): void => {
useEffect(() => {
// Define the handler function that will be called on the event.
const handleDeviceChange = () => {
console.log(
'[Renderer] `devicechange` event detected. Notifying main process.',
)
window.api.send('audio-devices-changed')
}
// Add the event listener when the component mounts.
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange)
// Return a cleanup function to remove the listener when the component unmounts.
// This is crucial for preventing memory leaks and ensuring good practice.
return () => {
navigator.mediaDevices.removeEventListener(
'devicechange',
handleDeviceChange,
)
console.log('[useDeviceChangeListener] Removed devicechange listener.')
}
}, []) // The empty dependency array ensures this effect runs only once on mount.
}
================================================
FILE: app/hooks/usePlatform.ts
================================================
import { useState, useEffect } from 'react'
type Platform = 'darwin' | 'win32'
export function usePlatform(): Platform | undefined {
const [platform, setPlatform] = useState(undefined)
useEffect(() => {
window.api.getPlatform().then(setPlatform)
}, [])
return platform
}
================================================
FILE: app/index.d.ts
================================================
///
declare module '*.css' {
const content: string
export default content
}
declare module '*.png' {
const content: string
export default content
}
declare module '*.jpg' {
const content: string
export default content
}
declare module '*.jpeg' {
const content: string
export default content
}
declare module '*.svg' {
const content: string
export default content
}
declare module '*.webm' {
const content: string
export default content
}
declare module '*.web' {
const content: string
export default content
}
// Augment the Window interface
declare global {
interface Window {
api: IpcApi
}
}
export interface IpcApi {
generateNewAuthState: () => Promise
invoke: (channel: string, ...args: any[]) => Promise
on: (
channel: string,
listener: (event: any, ...args: any[]) => void,
) => () => void // Returns a cleanup function
send: (channel: string, ...args: any[]) => void
getNativeAudioDevices: () => Promise
notifyLoginSuccess: (
profile: any,
idToken: string,
accessToken: string,
) => void
deleteUserData: () => Promise
}
================================================
FILE: app/index.html
================================================
Ito
================================================
FILE: app/media/microphone.ts
================================================
import { useSettingsStore } from '../store/useSettingsStore'
type Microphone = {
deviceId: string
label: string
}
type MicrophoneToRender = {
title: string
description?: string
}
async function getAvailableMicrophones(): Promise {
try {
console.log('Fetching available native microphones...')
// This now gets the list directly from our Rust binary via the main process
const deviceNames: string[] = await window.api.invoke(
'get-native-audio-devices',
)
console.log('Available native microphones:', deviceNames)
// The deviceId and label are the same in this new system
return deviceNames.map(name => ({
deviceId: name,
label: name,
}))
} catch (error) {
console.error('Error getting available native microphones:', error)
return []
}
}
/**
* Verifies if the currently selected microphone in settings is still connected.
* If not, it gracefully falls back to the "default" auto-detect setting.
*/
export async function verifyStoredMicrophone() {
try {
console.log('[verifyStoredMicrophone] Verifying selected microphone...')
const { microphoneDeviceId, setMicrophoneDeviceId } =
useSettingsStore.getState()
// If the user already has "default" selected, there's nothing to verify.
if (microphoneDeviceId === 'default') {
console.log(
'[verifyStoredMicrophone] "Auto-detect" is selected. Verification not needed.',
)
return
}
// Get the list of currently available microphones from the native backend.
const availableDevices: string[] = await window.api.invoke(
'get-native-audio-devices',
)
// Check if the stored deviceId is in the list of available devices.
const isDeviceAvailable = availableDevices.includes(microphoneDeviceId)
if (isDeviceAvailable) {
console.log(
`[verifyStoredMicrophone] Stored microphone "${microphoneDeviceId}" is still available.`,
)
} else {
console.warn(
`[verifyStoredMicrophone] Stored microphone "${microphoneDeviceId}" is not available. Falling back to "Auto-detect".`,
)
// The device is disconnected. Update the store to use the default.
// We pass the friendly name "Auto-detect" to keep the UI consistent.
setMicrophoneDeviceId('default', 'Auto-detect')
}
} catch (error) {
console.error(
'[verifyStoredMicrophone] Failed to verify microphone:',
error,
)
}
}
const microphoneToRender = (microphone: Microphone): MicrophoneToRender => {
const label = microphone.label.toLowerCase()
// Handle default device case
if (label.includes('default -')) {
return {
title: `Auto-detect`,
description:
'May connect to Bluetooth earbuds, slowing transcription speed',
}
}
// Handle built-in microphone
if (label.includes('built-in') || label.includes('macbook pro microphone')) {
return {
title: 'Built-in mic (recommended)',
}
}
// Default case - return original label
return {
title: microphone.label,
}
}
export { getAvailableMicrophones, microphoneToRender }
export type { Microphone, MicrophoneToRender }
================================================
FILE: app/renderer.tsx
================================================
import './sentry'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './app'
if (window.location.hash !== '#/pill') {
import('@/app/styles/app.css')
}
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
,
)
================================================
FILE: app/sentry.ts
================================================
import * as Sentry from '@sentry/electron/renderer'
const dsn = import.meta.env.VITE_SENTRY_DSN as string | undefined
const environment =
(import.meta.env.VITE_SENTRY_ENV as string | undefined) || 'local'
const tracesSampleRate = Number.parseFloat(
(import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE as string | undefined) ||
'0.2',
)
const profilesSampleRate = Number.parseFloat(
(import.meta.env.VITE_SENTRY_PROFILES_SAMPLE_RATE as string | undefined) ||
'0.2',
)
Sentry.init({
enabled: Boolean(dsn),
dsn,
environment,
tracesSampleRate,
profilesSampleRate,
beforeBreadcrumb: breadcrumb =>
breadcrumb?.category === 'console' ? null : breadcrumb,
integrations: integrations =>
integrations.filter(integration =>
typeof (integration as any).name === 'string'
? !(
(integration as any).name.toLowerCase().includes('console') ||
(integration as any).name.toLowerCase() === 'breadcrumbs'
)
: true,
),
})
================================================
FILE: app/store/useAdvancedSettingsStore.ts
================================================
import { create } from 'zustand'
import { STORE_KEYS } from '../../lib/constants/store-keys'
export interface LlmSettings {
asrProvider: string | null
asrModel: string | null
asrPrompt: string | null
llmProvider: string | null
llmModel: string | null
llmTemperature: number | null
transcriptionPrompt: string | null
editingPrompt: string | null
noSpeechThreshold: number | null
}
interface AdvancedSettingsState {
llm: LlmSettings
grammarServiceEnabled: boolean
defaults?: LlmSettings
macosAccessibilityContextEnabled: boolean
setLlmSettings: (settings: Partial) => void
setGrammarServiceEnabled: (enabled: boolean) => void
setMacosAccessibilityContextEnabled: (enabled: boolean) => void
}
// Initialize from electron store
const getInitialState = () => {
const storedAdvancedSettings = window.electron.store.get(
STORE_KEYS.ADVANCED_SETTINGS,
)
return {
llm: storedAdvancedSettings.llm,
grammarServiceEnabled:
storedAdvancedSettings.grammarServiceEnabled ?? false,
defaults: storedAdvancedSettings.defaults,
macosAccessibilityContextEnabled:
storedAdvancedSettings.macosAccessibilityContextEnabled ?? false,
}
}
// Sync to electron store
const syncToStore = (state: Partial) => {
const currentAdvancedSettings =
window.electron.store.get(STORE_KEYS.ADVANCED_SETTINGS) || {}
const updatedAdvancedSettings = {
...currentAdvancedSettings,
...state,
}
window.electron.store.set(
STORE_KEYS.ADVANCED_SETTINGS,
updatedAdvancedSettings,
)
}
export const useAdvancedSettingsStore = create(set => {
const initialState = getInitialState()
// Subscribe to updates from sync service
const handleStoreUpdate = () => {
const latestState = getInitialState()
set(latestState)
}
window.api.on('advanced-settings-updated', handleStoreUpdate)
return {
...initialState,
setLlmSettings: (settings: Partial) => {
set(state => {
const newLlmSettings = { ...state.llm, ...settings }
const partialState = { llm: newLlmSettings }
syncToStore(partialState)
return partialState
})
},
setGrammarServiceEnabled: (enabled: boolean) => {
set(() => {
const partialState = { grammarServiceEnabled: enabled }
syncToStore(partialState)
return partialState
})
},
setMacosAccessibilityContextEnabled: (enabled: boolean) => {
set(() => {
const partialState = { macosAccessibilityContextEnabled: enabled }
syncToStore(partialState)
return partialState
})
},
}
})
================================================
FILE: app/store/useAudioStore.ts
================================================
import { create } from 'zustand'
import log from 'electron-log'
interface AudioState {
isRecording: boolean
isShortcutEnabled: boolean
setIsShortcutEnabled: (enabled: boolean) => void
startRecording: () => Promise
stopRecording: () => Promise
}
export const useAudioStore = create((set, get) => ({
isRecording: false,
isShortcutEnabled: true,
setIsShortcutEnabled: (enabled: boolean) => {
set({ isShortcutEnabled: enabled })
},
startRecording: async () => {
const { isRecording, isShortcutEnabled } = get()
if (isRecording || !isShortcutEnabled) return
console.log('[AudioStore] Starting native recording...')
set({ isRecording: true })
// Signal the main process to start the gRPC stream and tell the
// native recorder to begin capturing.
window.api.send('start-native-recording')
},
stopRecording: async () => {
const { isRecording } = get()
if (!isRecording) return
console.log('[AudioStore] Stopping native recording...')
// Signal the main process to tell the native recorder to stop
// and to close the gRPC stream.
window.api.send('stop-native-recording')
set({ isRecording: false })
},
}))
================================================
FILE: app/store/useAuthStore.ts
================================================
import { create } from 'zustand'
import type {
AuthState,
AuthUser,
AuthTokens,
AuthStore,
} from '../../lib/main/store'
import { STORE_KEYS } from '../../lib/constants/store-keys'
interface AuthZustandStore {
// State
isAuthenticated: boolean
user: AuthUser | null
tokens: AuthTokens | null
state: AuthState | null
isLoading: boolean
error: string | null
isSelfHosted: boolean
// Actions
setAuthData: (tokens: AuthTokens, user: AuthUser, provider?: string) => void
clearAuth: (preserveUser?: boolean) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
updateUser: (user: Partial) => void
updateState: (state: Partial) => void
setName: (name: string) => void
setSelfHostedMode: () => void
}
// Initialize from electron store
const getInitialState = () => {
const storedAuth = window.electron?.store?.get(STORE_KEYS.AUTH) as
| (AuthStore & { isSelfHosted?: boolean })
| undefined
// Generate new auth state if no stored auth stat
return {
isAuthenticated:
!!storedAuth?.tokens?.access_token || !!storedAuth?.isSelfHosted,
user: storedAuth?.user || null,
tokens: storedAuth?.tokens || null,
state: storedAuth?.state || null,
isLoading: false,
error: null,
isSelfHosted: !!storedAuth?.isSelfHosted,
}
}
// Sync to electron store
const syncToStore = (state: {
user?: AuthUser | null
tokens?: AuthTokens | null
state?: AuthState | null
isSelfHosted?: boolean
}) => {
if (!window.electron?.store) return
const currentStore = window.electron.store.get(STORE_KEYS.AUTH) || {}
const updates: any = { ...currentStore }
if ('user' in state) {
updates.user = state.user
}
if ('tokens' in state) {
updates.tokens = state.tokens
}
if ('state' in state) {
updates.state = state.state
}
if ('isSelfHosted' in state) {
updates.isSelfHosted = state.isSelfHosted
}
window.electron.store.set(STORE_KEYS.AUTH, updates)
}
export const useAuthStore = create((set, get) => {
const initialState = getInitialState()
return {
...initialState,
setAuthData: (tokens: AuthTokens, user: AuthUser, provider?: string) => {
// Calculate expires_at if not provided
const expiresAt =
tokens.expires_at ||
(tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined)
const enhancedUser: AuthUser = {
...user,
provider,
lastSignInAt: new Date().toISOString(),
}
const enhancedTokens = {
...tokens,
expires_at: expiresAt,
}
const newState = {
isAuthenticated: true,
tokens: enhancedTokens,
user: enhancedUser,
state: get().state || null,
error: null,
}
syncToStore({ tokens: enhancedTokens, user: enhancedUser })
set(newState)
},
clearAuth: (preserveUser: boolean = true) => {
const currentUser = get().user
const newState = {
isAuthenticated: false,
user: preserveUser ? currentUser : null,
tokens: null,
state: null,
error: null,
isSelfHosted: false,
}
syncToStore({
tokens: null,
user: preserveUser ? currentUser : null,
state: null,
isSelfHosted: false,
})
set(newState)
},
setLoading: (loading: boolean) => {
set({ isLoading: loading })
},
setError: (error: string | null) => {
set({ error })
},
updateUser: (userUpdate: Partial) => {
const currentUser = get().user
if (!currentUser) return
const updatedUser = { ...currentUser, ...userUpdate }
syncToStore({ user: updatedUser })
set({ user: updatedUser })
},
setName: (name: string) => {
const currentUser = get().user
if (!currentUser) return
const updatedUser = { ...currentUser, name }
syncToStore({ user: updatedUser })
set({ user: updatedUser })
},
updateState: (stateUpdate: Partial) => {
const currentState = get().state
if (!currentState) return
const updatedState = { ...currentState, ...stateUpdate }
syncToStore({ state: updatedState })
set({ state: updatedState })
},
setSelfHostedMode: () => {
const selfHostedUser: AuthUser = {
id: 'self-hosted',
provider: 'self-hosted',
lastSignInAt: new Date().toISOString(),
}
const newState = {
isAuthenticated: true,
isSelfHosted: true,
user: selfHostedUser,
tokens: null, // No tokens needed for self-hosted
error: null,
}
syncToStore({ user: selfHostedUser, isSelfHosted: true })
set(newState)
},
}
})
================================================
FILE: app/store/useDictionaryStore.ts
================================================
import { create } from 'zustand'
import { useAuthStore } from './useAuthStore'
import type { DictionaryItem } from '../../lib/main/sqlite/models'
export type DictionaryEntry = {
id: string
type: 'normal' | 'replacement'
createdAt: string // Changed to string to match DB
updatedAt: string // Changed to string to match DB
} & (
| {
type: 'normal'
content: string
}
| {
type: 'replacement'
from: string
to: string
}
)
interface DictionaryStore {
entries: DictionaryEntry[]
loadEntries: () => Promise
addEntry: (content: string) => Promise
addReplacement: (from: string, to: string) => Promise
updateEntry: (
id: string,
updates: Partial>,
) => Promise
deleteEntry: (id: string) => Promise
}
/**
* The backend stores a flat DictionaryItem, but the frontend uses a more
* structured DictionaryEntry. This function maps the backend type to the
* frontend type.
* We infer the type based on whether `pronunciation` is null.
*/
const mapItemToEntry = (item: DictionaryItem): DictionaryEntry => {
if (item.pronunciation === null || item.pronunciation === '') {
return {
id: item.id,
type: 'normal',
content: item.word,
createdAt: item.created_at,
updatedAt: item.updated_at,
}
} else {
return {
id: item.id,
type: 'replacement',
from: item.word,
to: item.pronunciation,
createdAt: item.created_at,
updatedAt: item.updated_at,
}
}
}
export const useDictionaryStore = create((set, get) => ({
entries: [],
loadEntries: async () => {
try {
const items = await window.api.dictionary.getAll()
const entries = items.map(mapItemToEntry)
set({ entries })
} catch (error) {
console.error('Failed to load dictionary from database:', error)
}
},
addEntry: async (content: string) => {
const { user } = useAuthStore.getState()
if (!user) return
const result = await window.api.dictionary.add({
user_id: user.id,
word: content.trim(),
pronunciation: null,
})
if (!result.success) {
throw new Error(result.error)
}
const newEntry = mapItemToEntry(result.data)
set(state => ({ entries: [newEntry, ...state.entries] }))
},
addReplacement: async (from: string, to: string) => {
const { user } = useAuthStore.getState()
if (!user) return
const result = await window.api.dictionary.add({
user_id: user.id,
word: from.trim(),
pronunciation: to.trim(),
})
if (!result.success) {
throw new Error(result.error)
}
const newEntry = mapItemToEntry(result.data)
set(state => ({ entries: [newEntry, ...state.entries] }))
},
updateEntry: async (id, updates) => {
const originalEntry = get().entries.find(e => e.id === id)
if (!originalEntry) return
// Create a new entry object with the updates applied
const updatedEntry = { ...originalEntry, ...updates }
let word: string
let pronunciation: string | null
if (updatedEntry.type === 'normal') {
word = updatedEntry.content
pronunciation = null
} else {
word = updatedEntry.from
pronunciation = updatedEntry.to
}
const result = await window.api.dictionary.update(id, word, pronunciation)
if (!result.success) {
throw new Error(result.error)
}
get().loadEntries() // Reload all entries to reflect the change
},
deleteEntry: async (id: string) => {
try {
await window.api.dictionary.delete(id)
set(state => ({ entries: state.entries.filter(e => e.id !== id) }))
} catch (error) {
console.error('Failed to delete dictionary entry:', error)
}
},
}))
================================================
FILE: app/store/useMainStore.ts
================================================
import { create } from 'zustand'
import { STORE_KEYS } from '../../lib/constants/store-keys'
type PageType = 'home' | 'dictionary' | 'notes' | 'settings' | 'about'
type SettingsPageType =
| 'general'
| 'keyboard'
| 'audio'
| 'account'
| 'advanced'
| 'pricing-billing'
interface MainStore {
navExpanded: boolean
currentPage: PageType
settingsPage: SettingsPageType
toggleNavExpanded: () => void
setCurrentPage: (page: PageType) => void
setSettingsPage: (page: SettingsPageType) => void
}
// Initialize from electron store
const getInitialState = () => {
const storedMain = window.electron.store.get(STORE_KEYS.MAIN)
return {
navExpanded: storedMain?.navExpanded ?? true,
currentPage: (storedMain?.currentPage as PageType) ?? 'home',
settingsPage: (storedMain?.settingsPage as SettingsPageType) ?? 'general',
}
}
// Sync to electron store
const syncToStore = (state: Partial) => {
const currentStore = window.electron.store.get(STORE_KEYS.MAIN) || {}
const updates: any = { ...currentStore }
if ('navExpanded' in state) {
updates.navExpanded = state.navExpanded ?? currentStore.navExpanded
}
if ('settingsPage' in state) {
updates.settingsPage = state.settingsPage ?? currentStore.settingsPage
}
window.electron.store.set(STORE_KEYS.MAIN, updates)
}
export const useMainStore = create(set => {
const initialState = getInitialState()
return {
navExpanded: initialState.navExpanded,
currentPage: 'home',
settingsPage: initialState.settingsPage,
toggleNavExpanded: () =>
set(state => {
const newState = { navExpanded: !state.navExpanded }
syncToStore(newState)
return newState
}),
setCurrentPage: (page: PageType) => set({ currentPage: page }),
setSettingsPage: (page: SettingsPageType) => {
const newState = { settingsPage: page }
syncToStore(newState)
set(newState)
},
}
})
================================================
FILE: app/store/useNotesStore.ts
================================================
import { create } from 'zustand'
import { useAuthStore } from './useAuthStore'
export type Note = {
id: string
content: string
user_id: string
interaction_id: string | null
created_at: string
updated_at: string
}
interface NotesStore {
notes: Note[]
loadNotes: () => Promise
addNote: (content: string) => Promise
updateNote: (id: string, content: string) => Promise
deleteNote: (id: string) => Promise
}
export const useNotesStore = create((set, get) => ({
notes: [],
loadNotes: async () => {
try {
const notes = await window.api.notes.getAll()
set({ notes })
} catch (error) {
console.error('Failed to load notes from database:', error)
}
},
addNote: async (content: string) => {
const { user } = useAuthStore.getState()
if (!user) {
console.error('Cannot add a note without a logged-in user.')
return
}
try {
const newNote = await window.api.notes.add({
content: content.trim(),
user_id: user.id,
})
set(state => ({ notes: [newNote, ...state.notes] }))
} catch (error) {
console.error('Failed to add note to database:', error)
}
},
updateNote: async (id: string, content: string) => {
try {
await window.api.notes.updateContent(id, content)
// For an immediate UI update, we can call loadNotes again
// or manually update the state.
get().loadNotes()
} catch (error) {
console.error('Failed to update note in database:', error)
}
},
deleteNote: async (id: string) => {
try {
await window.api.notes.delete(id)
set(state => ({
notes: state.notes.filter(note => note.id !== id),
}))
} catch (error) {
console.error('Failed to delete note from database:', error)
}
},
}))
================================================
FILE: app/store/useOnboardingStore.ts
================================================
import { create } from 'zustand'
import { analytics, ANALYTICS_EVENTS } from '../components/analytics'
import { STORE_KEYS } from '../../lib/constants/store-keys'
// Onboarding category constants
export const ONBOARDING_CATEGORIES = {
SIGN_UP: 'sign-up',
PERMISSIONS: 'permissions',
SET_UP: 'set-up',
TRY_IT: 'try-it',
} as const
// Export the type so it can be used in other files
export type OnboardingCategory =
(typeof ONBOARDING_CATEGORIES)[keyof typeof ONBOARDING_CATEGORIES]
interface OnboardingState {
onboardingStep: number
totalOnboardingSteps: number
onboardingCompleted: boolean
onboardingCategory: OnboardingCategory
referralSource: string | null
incrementOnboardingStep: () => void
decrementOnboardingStep: () => void
setReferralSource: (source: string) => void
setOnboardingCompleted: () => void
resetOnboarding: () => void
initializeOnboarding: () => void
}
// Step name constants
export const STEP_NAMES = {
CREATE_ACCOUNT: 'create_account',
REFERRAL_SOURCE: 'referral_source',
DATA_CONTROL: 'data_control',
PERMISSIONS: 'permissions',
MICROPHONE_TEST: 'microphone_test',
KEYBOARD_TEST: 'keyboard_test',
GOOD_TO_GO: 'good_to_go',
INTRODUCING_INTELLIGENT_MODE: 'introducing_intelligent_mode',
ANY_APP: 'any_app',
TRY_IT_OUT: 'try_it_out',
}
// Order here matters for onboarding flow
export const STEP_NAMES_ARRAY = [
STEP_NAMES.CREATE_ACCOUNT,
STEP_NAMES.REFERRAL_SOURCE,
STEP_NAMES.DATA_CONTROL,
STEP_NAMES.PERMISSIONS,
STEP_NAMES.MICROPHONE_TEST,
STEP_NAMES.KEYBOARD_TEST,
STEP_NAMES.GOOD_TO_GO,
STEP_NAMES.INTRODUCING_INTELLIGENT_MODE,
STEP_NAMES.ANY_APP,
STEP_NAMES.TRY_IT_OUT,
]
const getOnboardingCategory = (onboardingStep: number): OnboardingCategory => {
if (onboardingStep < 3) return ONBOARDING_CATEGORIES.SIGN_UP
if (onboardingStep < 4) return ONBOARDING_CATEGORIES.PERMISSIONS
if (onboardingStep < 7) return ONBOARDING_CATEGORIES.SET_UP
return ONBOARDING_CATEGORIES.TRY_IT
}
export const getOnboardingCategoryIndex = (
onboardingCategory: OnboardingCategory,
): number => {
if (onboardingCategory === ONBOARDING_CATEGORIES.SIGN_UP) return 0
if (onboardingCategory === ONBOARDING_CATEGORIES.PERMISSIONS) return 1
if (onboardingCategory === ONBOARDING_CATEGORIES.SET_UP) return 2
return 3
}
const getStepName = (step: number): string => {
return STEP_NAMES_ARRAY[step] || 'unknown'
}
// Initialize from electron store
const getInitialState = () => {
const storedOnboarding = window.electron.store.get(STORE_KEYS.ONBOARDING)
return {
onboardingStep: storedOnboarding?.onboardingStep ?? 0,
onboardingCompleted: storedOnboarding?.onboardingCompleted ?? false,
}
}
// Sync to electron store
const syncToStore = (state: Partial) => {
if ('onboardingStep' in state || 'onboardingCompleted' in state) {
const currentStore = window.electron.store.get(STORE_KEYS.ONBOARDING) || {}
window.electron.store.set(STORE_KEYS.ONBOARDING, {
...currentStore,
onboardingStep: state.onboardingStep ?? currentStore.onboardingStep,
onboardingCompleted:
state.onboardingCompleted ?? currentStore.onboardingCompleted,
})
window.api.notifyOnboardingUpdate(state)
}
}
export const useOnboardingStore = create(set => {
const initialState = getInitialState()
const totalOnboardingSteps = STEP_NAMES_ARRAY.length
return {
onboardingStep: initialState.onboardingStep,
totalOnboardingSteps,
onboardingCompleted: initialState.onboardingCompleted,
onboardingCategory: getOnboardingCategory(initialState.onboardingStep),
referralSource: null,
incrementOnboardingStep: () =>
set(state => {
const onboardingStep = Math.min(
state.onboardingStep + 1,
state.totalOnboardingSteps,
)
const onboardingCategory = getOnboardingCategory(onboardingStep)
const newState = {
onboardingStep,
onboardingCategory,
}
// Track onboarding step completion
analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_STEP_COMPLETED, {
step: state.onboardingStep, // The step that was just completed
step_name: getStepName(state.onboardingStep),
category: state.onboardingCategory,
total_steps: state.totalOnboardingSteps,
referral_source: state.referralSource || undefined,
})
// Track viewing of new step
if (onboardingStep < state.totalOnboardingSteps) {
analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_STEP_VIEWED, {
step: onboardingStep,
step_name: getStepName(onboardingStep),
category: onboardingCategory,
total_steps: state.totalOnboardingSteps,
referral_source: state.referralSource || undefined,
})
}
syncToStore(newState)
return newState
}),
decrementOnboardingStep: () =>
set(state => {
const onboardingStep = Math.max(state.onboardingStep - 1, 0)
const onboardingCategory = getOnboardingCategory(onboardingStep)
const newState = {
onboardingStep,
onboardingCategory,
}
// Track viewing of previous step
analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_STEP_VIEWED, {
step: onboardingStep,
step_name: getStepName(onboardingStep),
category: onboardingCategory,
total_steps: state.totalOnboardingSteps,
referral_source: state.referralSource || undefined,
})
syncToStore(newState)
return newState
}),
setOnboardingCompleted: () =>
set(state => {
const step = state.totalOnboardingSteps - 1
analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_STEP_COMPLETED, {
step: state.totalOnboardingSteps,
step_name: getStepName(step),
category: getOnboardingCategory(step),
total_steps: state.totalOnboardingSteps,
})
analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_COMPLETED, {
step: state.totalOnboardingSteps,
step_name: 'completed',
category: ONBOARDING_CATEGORIES.TRY_IT,
total_steps: state.totalOnboardingSteps,
})
// Update user properties to mark onboarding as completed
analytics.updateUserProperties({
onboarding_completed: true,
referral_source: state.referralSource || undefined,
})
const newState = { onboardingCompleted: true }
syncToStore(newState)
return newState
}),
resetOnboarding: () =>
set(_state => {
const newState = { onboardingStep: 0, onboardingCompleted: false }
analytics.updateUserProperties({
onboarding_completed: false,
})
syncToStore(newState)
return newState
}),
setReferralSource: (source: string) =>
set(_state => {
const newState = { referralSource: source }
analytics.updateUserProperties({
referral_source: source,
})
syncToStore(newState)
return newState
}),
initializeOnboarding: () => {
const step = 0
const onboardingCategory = getOnboardingCategory(step)
analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_STEP_VIEWED, {
step,
step_name: getStepName(0),
category: onboardingCategory,
total_steps: totalOnboardingSteps,
})
},
}
})
================================================
FILE: app/store/usePermissionsStore.ts
================================================
import { create } from 'zustand'
interface PermissionsState {
isAccessibilityEnabled: boolean
isMicrophoneEnabled: boolean
setAccessibilityEnabled: (enabled: boolean) => void
setMicrophoneEnabled: (enabled: boolean) => void
}
export const usePermissionsStore = create(set => ({
isAccessibilityEnabled: false,
isMicrophoneEnabled: false,
setAccessibilityEnabled: enabled => set({ isAccessibilityEnabled: enabled }),
setMicrophoneEnabled: enabled => set({ isMicrophoneEnabled: enabled }),
}))
================================================
FILE: app/store/useSettingsStore.ts
================================================
import { create } from 'zustand'
import {
analytics,
ANALYTICS_EVENTS,
updateAnalyticsFromSettings,
} from '@/app/components/analytics'
import { STORE_KEYS } from '../../lib/constants/store-keys'
import type { KeyboardShortcutConfig } from '@/lib/main/store'
import { ItoMode } from '../generated/ito_pb'
import { ITO_MODE_SHORTCUT_DEFAULTS } from '@/lib/constants/keyboard-defaults'
import {
normalizeChord,
ShortcutResult,
validateShortcutForDuplicate,
isReservedCombination,
} from '../utils/keyboard'
import { KeyName } from '@/lib/types/keyboard'
interface SettingsState {
shareAnalytics: boolean
launchAtLogin: boolean
showItoBarAlways: boolean
showAppInDock: boolean
interactionSounds: boolean
muteAudioWhenDictating: boolean
microphoneDeviceId: string
microphoneName: string
keyboardShortcuts: KeyboardShortcutConfig[]
setShareAnalytics: (share: boolean) => void
setLaunchAtLogin: (launch: boolean) => void
setShowItoBarAlways: (show: boolean) => void
setShowAppInDock: (show: boolean) => void
setInteractionSounds: (enabled: boolean) => void
setMuteAudioWhenDictating: (enabled: boolean) => void
setMicrophoneDeviceId: (deviceId: string, name: string) => void
createKeyboardShortcut: (mode: ItoMode) => ShortcutResult
removeKeyboardShortcut: (shortcutId: string) => void
getItoModeShortcuts: (mode: ItoMode) => KeyboardShortcutConfig[]
updateKeyboardShortcut: (
shortcutId: string,
keys: KeyName[],
) => Promise
}
type SettingCategory = 'general' | 'audio&mic' | 'keyboard' | 'account'
// Initialize from electron store
const getInitialState = () => {
const storedSettings = window.electron.store.get(STORE_KEYS.SETTINGS)
return {
shareAnalytics: storedSettings?.shareAnalytics ?? true,
launchAtLogin: storedSettings?.launchAtLogin ?? true,
showItoBarAlways: storedSettings?.showItoBarAlways ?? true,
showAppInDock: storedSettings?.showAppInDock ?? true,
interactionSounds: storedSettings?.interactionSounds ?? false,
muteAudioWhenDictating: storedSettings?.muteAudioWhenDictating ?? false,
microphoneDeviceId: storedSettings?.microphoneDeviceId ?? 'default',
microphoneName: storedSettings?.microphoneName ?? 'Default Microphone',
keyboardShortcuts: storedSettings?.keyboardShortcuts ?? [
{
keys: ITO_MODE_SHORTCUT_DEFAULTS[ItoMode.EDIT],
mode: ItoMode.EDIT,
id: crypto.randomUUID(),
},
{
keys: ITO_MODE_SHORTCUT_DEFAULTS[ItoMode.TRANSCRIBE],
mode: ItoMode.TRANSCRIBE,
id: crypto.randomUUID(),
},
],
firstName: storedSettings?.firstName ?? '',
lastName: storedSettings?.lastName ?? '',
email: storedSettings?.email ?? '',
}
}
// --- START: CORRECTED CODE ---
// Sync to electron store
const syncToStore = (state: Partial) => {
const currentSettings = window.electron.store.get(STORE_KEYS.SETTINGS) || {}
// A much simpler and more robust way to merge the settings.
// This takes all existing settings and overwrites them with only the keys
// present in the new partial state, without accidentally unsetting others.
const updatedSettings = {
...currentSettings,
...state,
}
window.electron.store.set(STORE_KEYS.SETTINGS, updatedSettings)
// Notify pill window of settings changes
if (window.api?.notifySettingsUpdate) {
window.api.notifySettingsUpdate(updatedSettings)
}
// Re-register hotkeys when keyboard shortcuts change
if ('keyboardShortcuts' in state && window.api?.registerHotkeys) {
window.api.registerHotkeys()
}
}
export const useSettingsStore = create(set => {
const initialState = getInitialState()
// Helper for single-property setters
const createSetter =
(
key: K,
settingCategory: SettingCategory = 'general',
) =>
(value: SettingsState[K]) => {
const currentValue = useSettingsStore.getState()[key]
const partialState = { [key]: value } as Partial
analytics.trackSettings(ANALYTICS_EVENTS.SETTING_CHANGED, {
setting_name: key as string,
old_value: currentValue,
new_value: value,
setting_category: settingCategory,
})
set(partialState)
syncToStore(partialState)
}
return {
...initialState,
setShareAnalytics: (share: boolean) => {
const partialState = { shareAnalytics: share }
set(partialState)
syncToStore(partialState)
// Update analytics when setting changes
updateAnalyticsFromSettings(share)
},
setLaunchAtLogin: (launch: boolean) => {
const currentValue = useSettingsStore.getState().launchAtLogin
const partialState = { launchAtLogin: launch }
analytics.trackSettings(ANALYTICS_EVENTS.SETTING_CHANGED, {
setting_name: 'launchAtLogin',
old_value: currentValue,
new_value: launch,
setting_category: 'general',
})
set(partialState)
syncToStore(partialState)
if (window.api?.loginItem?.setSettings) {
window.api.loginItem.setSettings(launch)
}
},
setShowItoBarAlways: createSetter('showItoBarAlways', 'general'),
setShowAppInDock: (show: boolean) => {
const currentValue = useSettingsStore.getState().showAppInDock
const partialState = { showAppInDock: show }
// Track setting change
analytics.trackSettings(ANALYTICS_EVENTS.SETTING_CHANGED, {
setting_name: 'showAppInDock',
old_value: currentValue,
new_value: show,
setting_category: 'ui',
})
set(partialState)
syncToStore(partialState)
if (window.api?.dock?.setVisibility) {
window.api.dock.setVisibility(show)
}
},
setInteractionSounds: createSetter('interactionSounds', 'audio&mic'),
setMuteAudioWhenDictating: createSetter(
'muteAudioWhenDictating',
'audio&mic',
),
setMicrophoneDeviceId: (deviceId: string, name: string) => {
const currentName = useSettingsStore.getState().microphoneName
analytics.trackSettings(ANALYTICS_EVENTS.MICROPHONE_CHANGED, {
setting_name: 'microphoneName',
old_value: currentName,
new_value: name,
setting_category: 'audio&mic',
})
const partialState = {
microphoneDeviceId: deviceId,
microphoneName: name,
}
set(partialState)
syncToStore(partialState)
},
createKeyboardShortcut: (mode: ItoMode): ShortcutResult => {
const currentShortcuts = useSettingsStore.getState().keyboardShortcuts
const newShortcut = {
keys: [],
mode,
id: crypto.randomUUID(),
}
const newShortcuts = [...currentShortcuts, newShortcut]
const partialState = {
keyboardShortcuts: newShortcuts,
}
// Track keyboard shortcut change
analytics.trackSettings(ANALYTICS_EVENTS.KEYBOARD_SHORTCUTS_CHANGED, {
setting_name: 'keyboardShortcuts',
old_value: currentShortcuts,
new_value: newShortcuts,
setting_category: 'input',
})
// Update user properties
analytics.updateUserProperties({
keyboard_shortcuts: newShortcuts.map(ks => JSON.stringify(ks)),
})
set(partialState)
syncToStore(partialState)
return { success: true }
},
removeKeyboardShortcut: (shortcutId: string) => {
const currentShortcuts = useSettingsStore.getState().keyboardShortcuts
const newShortcuts = currentShortcuts.filter(ks => ks.id !== shortcutId)
const partialState = {
keyboardShortcuts: newShortcuts,
}
// Track keyboard shortcut change
analytics.trackSettings(ANALYTICS_EVENTS.KEYBOARD_SHORTCUTS_CHANGED, {
setting_name: 'keyboardShortcuts',
old_value: currentShortcuts,
new_value: newShortcuts,
setting_category: 'input',
})
// Update user properties
analytics.updateUserProperties({
keyboard_shortcuts: newShortcuts.map(ks => JSON.stringify(ks)),
})
set(partialState)
syncToStore(partialState)
},
getItoModeShortcuts: (mode: ItoMode) => {
const { keyboardShortcuts } = useSettingsStore.getState()
return keyboardShortcuts.filter(ks => ks.mode === mode)
},
updateKeyboardShortcut: async (
shortcutId: string,
keys: KeyName[],
): Promise => {
const currentShortcuts = useSettingsStore.getState()
.keyboardShortcuts as KeyboardShortcutConfig[]
const shortcut = currentShortcuts.find(ks => ks.id === shortcutId)
if (!shortcut) {
return { success: false, error: 'not-found' }
}
const normalizedKeys = normalizeChord(keys)
// Get platform for validation
const platform = await window.api.getPlatform()
// Check for reserved combinations
const reservedCheck = isReservedCombination(normalizedKeys, platform)
if (reservedCheck.isReserved) {
return {
success: false,
error: 'reserved-combination',
errorMessage: reservedCheck.reason,
}
}
const newShortcut = {
...shortcut,
keys: normalizedKeys,
}
const duplicateError = validateShortcutForDuplicate(
currentShortcuts,
newShortcut,
shortcut.mode,
)
if (duplicateError) {
return duplicateError
}
const updatedShortcuts = currentShortcuts.map(ks =>
ks.id === shortcutId ? { ...ks, keys: normalizedKeys } : ks,
)
const partialState = {
keyboardShortcuts: updatedShortcuts,
}
// Track keyboard shortcut change
analytics.trackSettings(ANALYTICS_EVENTS.KEYBOARD_SHORTCUTS_CHANGED, {
setting_name: 'keyboardShortcuts',
old_value: currentShortcuts,
new_value: updatedShortcuts,
setting_category: 'input',
})
// Update user properties
analytics.updateUserProperties({
keyboard_shortcuts: updatedShortcuts.map(ks => JSON.stringify(ks)),
})
set(partialState)
syncToStore(partialState)
return { success: true }
},
}
})
if (typeof window !== 'undefined' && window.api?.loginItem?.getSettings) {
window.api.loginItem
.getSettings()
.then(settings => {
const storedSettings = window.electron.store.get(STORE_KEYS.SETTINGS)
if (settings.openAtLogin !== storedSettings?.launchAtLogin) {
useSettingsStore.getState().setLaunchAtLogin(settings.openAtLogin)
}
})
.catch(error => {
console.error(
'Failed to sync login item settings on initialization:',
error,
)
})
}
if (typeof window !== 'undefined' && window.api?.dock?.getVisibility) {
window.api.invoke('init-window').then((windowInfo: any) => {
if (windowInfo.platform === 'darwin') {
window.api.dock
.getVisibility()
.then(dockSettings => {
const storedSettings = window.electron.store.get(STORE_KEYS.SETTINGS)
if (dockSettings.isVisible !== storedSettings?.showAppInDock) {
useSettingsStore.getState().setShowAppInDock(dockSettings.isVisible)
}
})
.catch(error => {
console.error(
'Failed to sync dock visibility on initialization:',
error,
)
})
}
})
}
================================================
FILE: app/store/useShortcutEditingStore.ts
================================================
import { create } from 'zustand'
interface ShortcutEditingState {
activeEditor: string | null
start: (editorKey: string) => boolean
stop: (editorKey: string) => void
isActive: (editorKey: string) => boolean
}
export const useShortcutEditingStore = create(
(set, get) => ({
activeEditor: null,
start: (editorKey: string): boolean => {
const current = get().activeEditor
if (current && current !== editorKey) return false
if (current === editorKey) return true
set({ activeEditor: editorKey })
return true
},
stop: (editorKey: string): void => {
const current = get().activeEditor
if (current === editorKey) {
set({ activeEditor: null })
}
},
isActive: (editorKey: string): boolean => get().activeEditor === editorKey,
}),
)
================================================
FILE: app/store/useUserMetadataStore.ts
================================================
import { create } from 'zustand'
import { useAuthStore } from './useAuthStore'
import type { UserMetadata } from '../../lib/main/sqlite/models'
import { PaidStatus } from '../../lib/main/sqlite/models'
interface UserMetadataStore {
metadata: UserMetadata | null
isLoading: boolean
loadMetadata: () => Promise
updateMetadata: (
updates: Partial>,
) => Promise
setPaidStatus: (status: PaidStatus) => Promise
setFreeWords: (count: number | null) => Promise
setProTrialStartDate: (date: Date | null) => Promise
setProTrialEndDate: (date: Date | null) => Promise
setProSubscriptionStartDate: (date: Date | null) => Promise
setProSubscriptionEndDate: (date: Date | null) => Promise
}
// Default state for new free users
const DEFAULT_METADATA = {
paid_status: PaidStatus.FREE,
free_words_remaining: 4000,
pro_trial_start_date: null,
pro_trial_end_date: null,
pro_subscription_start_date: null,
pro_subscription_end_date: null,
}
export const useUserMetadataStore = create((set, get) => ({
metadata: null,
isLoading: false,
loadMetadata: async () => {
try {
set({ isLoading: true })
const metadata = await window.api.userMetadata.get()
// If no metadata exists for this user, create default
if (!metadata) {
const { user } = useAuthStore.getState()
if (!user?.id) return
const now = new Date()
const newMetadata: UserMetadata = {
id: crypto.randomUUID(),
user_id: user.id,
...DEFAULT_METADATA,
created_at: now,
updated_at: now,
}
await window.api.userMetadata.upsert(newMetadata)
set({ metadata: newMetadata, isLoading: false })
} else {
set({ metadata, isLoading: false })
}
} catch (error) {
console.error('Failed to load user metadata from database:', error)
set({ isLoading: false })
}
},
updateMetadata: async updates => {
try {
await window.api.userMetadata.update(updates)
await get().loadMetadata() // Reload to get fresh data
} catch (error) {
console.error('Failed to update user metadata:', error)
throw error
}
},
setPaidStatus: async (status: PaidStatus) => {
await get().updateMetadata({ paid_status: status })
},
setFreeWords: async (count: number | null) => {
await get().updateMetadata({ free_words_remaining: count })
},
setProTrialStartDate: async (date: Date | null) => {
await get().updateMetadata({ pro_trial_start_date: date })
},
setProTrialEndDate: async (date: Date | null) => {
await get().updateMetadata({ pro_trial_end_date: date })
},
setProSubscriptionStartDate: async (date: Date | null) => {
await get().updateMetadata({ pro_subscription_start_date: date })
},
setProSubscriptionEndDate: async (date: Date | null) => {
await get().updateMetadata({ pro_subscription_end_date: date })
},
}))
================================================
FILE: app/styles/app.css
================================================
@import './globals.css';
@import './window.css';
body {
font-family:
'Inter',
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
font-size: 14px;
margin: 0;
overflow: hidden;
}
html,
body,
#app {
height: 100%;
margin: 0;
line-height: 1.4;
}
================================================
FILE: app/styles/globals.css
================================================
@import 'tailwindcss';
@source '@/app';
@source '@/lib';
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}
@layer base {
:root {
--background: var(--color-neutral-50);
--foreground: var(--color-neutral-900);
--card: hsl(0 0% 100%);
--card-foreground: hsl(0 0% 3.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(0 0% 3.9%);
--primary: hsl(0 0% 9%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(0 0% 96.1%);
--secondary-foreground: hsl(0 0% 9%);
--muted: hsl(0 0% 96.1%);
--muted-foreground: hsl(0 0% 45.1%);
--accent: hsl(0 0% 96.1%);
--accent-foreground: hsl(0 0% 9%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 89.8%);
--input: hsl(0 0% 89.8%);
--ring: hsl(0 0% 3.9%);
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
--radius: 0.5rem;
}
/* Custom scrollbar styling */
* {
scrollbar-width: thin;
scrollbar-color: rgb(209 213 219) transparent;
}
*::-webkit-scrollbar {
width: 8px;
height: 8px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: rgb(209 213 219);
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: rgb(156 163 175);
}
.dark {
--background: var(--color-neutral-950);
--foreground: var(--color-neutral-100);
--card: hsl(0, 0%, 3.9%);
--card-foreground: hsl(0 0% 98%);
--popover: hsl(0 0% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--primary: hsl(0 0% 98%);
--primary-foreground: hsl(0 0% 9%);
--secondary: hsl(0 0% 14.9%);
--secondary-foreground: hsl(0 0% 98%);
--muted: hsl(0 0% 14.9%);
--muted-foreground: hsl(0 0% 63.9%);
--accent: hsl(0 0% 14.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 14.9%);
--input: hsl(0 0% 14.9%);
--ring: hsl(0 0% 83.1%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
}
/* Dark mode scrollbar */
.dark * {
scrollbar-color: rgb(64 64 64) transparent;
}
.dark *::-webkit-scrollbar-thumb {
background-color: rgb(64 64 64);
}
.dark *::-webkit-scrollbar-thumb:hover {
background-color: rgb(82 82 82);
}
}
@layer utilities {
.scrollbar-hide {
-ms-overflow-style: none;
/* Internet Explorer 10+ */
scrollbar-width: none;
/* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
/* Safari and Chrome */
}
}
================================================
FILE: app/styles/window.css
================================================
:root {
color-scheme: light;
--window-icon-height: 16px;
--window-title-margin: 42px;
--window-titlebar-height: 48px;
--window-titlebar-font-size: 13px;
--window-scrollbar-width: 12px;
--window-mac-titlebar-controls-margin: 80px;
--window-background-transition-duration: 0.3s;
--window-c-popup-font-weight: normal;
--window-titlebar-font-weight: 500;
--window-c-popup-font-weight: 500;
--window-c-background: #fff;
--window-c-titlebar-background: #fff;
--window-c-titlebar-border: #8080801a;
--window-c-text: #000000c8;
--window-c-hover: #ececec;
--window-c-popup-background: #f6f6f6;
--window-c-popup-border: #dcdcdc;
--window-c-popup-shadow: hsla(0, 0%, 0%, 0.1);
--window-c-separator: #80808033;
--window-c-control-hover: #0000001a;
--window-c-control-close-hover: #ff453b;
--window-c-control-close-hover-text: #fff;
--window-c-scrollbar-track: #e0e0e0;
--window-c-scrollbar-thumb: #b0b0b0;
--window-c-scrollbar-thumb-hover: #888888cf;
--window-c-text-shadow: transparent;
}
:root.dark {
color-scheme: dark;
--window-icon-height: 16px;
--window-title-margin: 42px;
--window-titlebar-height: 48px;
--window-titlebar-font-size: 13px;
--window-scrollbar-width: 12px;
--window-mac-titlebar-controls-margin: 80px;
--window-background-transition-duration: 0.3s;
--window-c-popup-font-weight: normal;
--window-c-background: #1c1c1c;
--window-c-titlebar-background: #282828;
--window-c-titlebar-border: transparent;
--window-c-text: #ffffffc8;
--window-c-hover: #3c3c3c;
--window-c-popup-background: #282828;
--window-c-popup-border: #3c3c3c;
--window-c-popup-shadow: #00000080;
--window-c-separator: #80808033;
--window-c-control-hover: #0000003d;
--window-c-control-close-hover: #c42b1c;
--window-c-scrollbar-track: #1e1e1ec6;
--window-c-scrollbar-thumb: #88888863;
--window-c-scrollbar-thumb-hover: #555;
--window-c-text-shadow: #000000a8;
}
.window-frame {
display: flex;
flex-direction: column;
user-select: none;
background-color: var(--window-c-background);
transition: background-color var(--window-background-transition-duration) ease;
}
.window-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative;
}
/* Assign last children to use full height of window content */
/* .window-content div:last-child {
height: 100%;
} */
.window-titlebar {
display: flex;
position: relative;
height: var(--window-titlebar-height);
align-items: center;
-webkit-app-region: drag;
background-color: var(--window-c-titlebar-background);
color: var(--window-c-text);
transition: background-color var(--window-background-transition-duration) ease;
border-bottom: 1px solid var(--window-c-titlebar-border);
}
.titlebar-action-btn {
-webkit-app-region: no-drag;
}
.window-titlebar-icon {
position: absolute;
left: 0;
top: 0;
width: var(--window-title-margin);
height: 100%;
padding: 0 10px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
}
.window-titlebar-icon img {
width: 100%;
max-width: 16px;
}
.window-titlebar-title {
flex: 1;
font-size: var(--window-titlebar-font-size);
margin-left: var(--window-title-margin);
font-weight: var(--window-titlebar-font-weight, normal);
padding-left: 4px;
}
.window-titlebar-title[data-centered] {
text-align: center;
padding-left: 0;
margin: 0;
}
.window-titlebar-controls {
display: flex;
position: absolute;
left: 0;
top: 0;
-webkit-app-region: no-drag;
z-index: 20;
}
.window-titlebar-menu {
display: flex;
flex-direction: row;
gap: 2px;
position: absolute;
top: 9px;
left: var(--window-title-margin);
-webkit-app-region: no-drag;
font-size: var(--window-titlebar-font-size);
}
.titlebar-menuItem .menuItem-label {
padding: 2px 8px;
cursor: pointer;
border-radius: 4px;
font-weight: var(--window-titlebar-font-weight, normal);
}
.titlebar-menuItem:hover .menuItem-label,
.titlebar-menuItem.active .menuItem-label {
background-color: var(--window-c-hover);
}
.titlebar-menuItem .menuItem-popup {
position: fixed;
background-color: var(--window-c-popup-background);
top: 32px;
min-width: 100px;
border: 1px solid var(--window-c-popup-border);
padding: 0.25rem 0;
box-shadow: 2px 1px 4px var(--window-c-popup-shadow);
z-index: 10000;
border-radius: 4px;
}
.titlebar-menuItem .menuItem-popupItem {
display: flex;
flex-direction: row;
padding: 5px 18px;
text-shadow: 1px 1px var(--window-c-text-shadow);
justify-content: space-between;
font-weight: var(--window-c-popup-font-weight);
}
.titlebar-menuItem .menuItem-shortcut {
opacity: 0.5;
margin-left: 3rem;
}
.titlebar-menuItem .menuItem-popupItem:hover {
background-color: var(--window-c-hover);
}
.titlebar-menuItem .menuItem-popupItem.menuItem-separator {
border-top: 1px solid var(--window-c-separator);
margin-top: 6px;
padding: 3px 0;
pointer-events: none;
}
.titlebar-controlButton {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 30px;
cursor: pointer;
background-color: transparent;
}
.titlebar-controlButton:hover {
background-color: var(--window-c-control-hover);
}
.titlebar-controlButton[aria-label='close']:hover {
background-color: var(--window-c-control-close-hover);
color: var(--window-c-control-close-hover-text, inherit);
}
/* Custom scrollbar for window content */
.window-content::-webkit-scrollbar {
width: var(--window-scrollbar-width);
}
.window-content::-webkit-scrollbar-track {
background-color: var(--window-c-scrollbar-track);
}
.window-content::-webkit-scrollbar-thumb {
background-color: var(--window-c-scrollbar-thumb);
border-radius: 4px;
border: 3px solid var(--window-c-scrollbar-track);
}
.window-content::-webkit-scrollbar-thumb:hover {
background-color: var(--window-c-scrollbar-thumb-hover);
}
.platform-darwin .window-titlebar-title {
margin-left: var(--window-mac-titlebar-controls-margin);
}
.platform-darwin .window-titlebar-menu {
left: var(--window-mac-titlebar-controls-margin);
}
================================================
FILE: app/utils/audioUtils.ts
================================================
export function createStereo48kWavFromMonoPCM(
pcm16le: Uint8Array,
srcRate = 16000,
targetRate = 48000,
bitsPerSample = 16,
): ArrayBuffer {
const sampleCount = Math.floor(pcm16le.length / 2)
const src = new Float32Array(sampleCount)
for (let i = 0, j = 0; i < sampleCount; i++, j += 2) {
let s = (pcm16le[j] | (pcm16le[j + 1] << 8)) & 0xffff
if (s & 0x8000) s = s - 0x10000
src[i] = Math.max(-1, Math.min(1, s / 32768))
}
const ratio = targetRate / srcRate
const outLen = Math.max(1, Math.floor(src.length * ratio))
const resampled = new Float32Array(outLen)
for (let i = 0; i < outLen; i++) {
const pos = i / ratio
const idx = Math.floor(pos)
const frac = pos - idx
const a = src[idx] ?? 0
const b = src[idx + 1] ?? a
resampled[i] = a + (b - a) * frac
}
const numChannels = 2
const interleaved = new Int16Array(outLen * numChannels)
for (let i = 0, j = 0; i < outLen; i++) {
const s = Math.max(-1, Math.min(1, resampled[i]))
const v = (s * 32767) | 0
interleaved[j++] = v
interleaved[j++] = v
}
const byteRate = (targetRate * numChannels * bitsPerSample) / 8
const blockAlign = (numChannels * bitsPerSample) / 8
const dataLength = interleaved.byteLength
const buffer = new ArrayBuffer(44 + dataLength)
const view = new DataView(buffer)
const writeString = (offset: number, string: string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i))
}
}
writeString(0, 'RIFF')
view.setUint32(4, 36 + dataLength, true)
writeString(8, 'WAVE')
writeString(12, 'fmt ')
view.setUint32(16, 16, true)
view.setUint16(20, 1, true)
view.setUint16(22, numChannels, true)
view.setUint32(24, targetRate, true)
view.setUint32(28, byteRate, true)
view.setUint16(32, blockAlign, true)
view.setUint16(34, bitsPerSample, true)
writeString(36, 'data')
view.setUint32(40, dataLength, true)
new Uint8Array(buffer).set(new Uint8Array(interleaved.buffer), 44)
return buffer
}
================================================
FILE: app/utils/healthCheck.test.ts
================================================
import { describe, test, expect, mock, beforeEach } from 'bun:test'
import { checkLocalServerHealth } from './healthCheck'
// Mock the window.api
const mockApi = {
checkServerHealth: mock(),
}
// Setup global window mock
const originalWindow = globalThis.window
beforeEach(() => {
globalThis.window = {
...originalWindow,
api: mockApi,
} as any
// Clear mock call history
mockApi.checkServerHealth.mockClear()
})
describe('checkLocalServerHealth', () => {
test('should return healthy status when server is healthy', async () => {
// Arrange
const mockResponse = {
isHealthy: true,
error: undefined,
}
mockApi.checkServerHealth.mockResolvedValue(mockResponse)
// Act
const result = await checkLocalServerHealth()
// Assert
expect(result).toEqual({
isHealthy: true,
error: undefined,
})
expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)
})
test('should return unhealthy status when server is not running', async () => {
// Arrange
const mockResponse = {
isHealthy: false,
error: 'Local server not running',
}
mockApi.checkServerHealth.mockResolvedValue(mockResponse)
// Act
const result = await checkLocalServerHealth()
// Assert
expect(result).toEqual({
isHealthy: false,
error: 'Local server not running',
})
expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)
})
test('should return unhealthy status when server returns invalid response', async () => {
// Arrange
const mockResponse = {
isHealthy: false,
error: 'Invalid server response',
}
mockApi.checkServerHealth.mockResolvedValue(mockResponse)
// Act
const result = await checkLocalServerHealth()
// Assert
expect(result).toEqual({
isHealthy: false,
error: 'Invalid server response',
})
expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)
})
test('should handle API call errors gracefully', async () => {
// Arrange
const error = new Error('IPC communication failed')
mockApi.checkServerHealth.mockRejectedValue(error)
// Act
const result = await checkLocalServerHealth()
// Assert
expect(result).toEqual({
isHealthy: false,
error: 'IPC communication failed',
})
expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)
})
test('should handle unknown error types', async () => {
// Arrange
mockApi.checkServerHealth.mockRejectedValue('Unknown error')
// Act
const result = await checkLocalServerHealth()
// Assert
expect(result).toEqual({
isHealthy: false,
error: 'Unknown error occurred',
})
expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)
})
test('should handle timeout scenarios', async () => {
// Arrange
const mockResponse = {
isHealthy: false,
error: 'Connection timed out',
}
mockApi.checkServerHealth.mockResolvedValue(mockResponse)
// Act
const result = await checkLocalServerHealth()
// Assert
expect(result).toEqual({
isHealthy: false,
error: 'Connection timed out',
})
expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)
})
})
================================================
FILE: app/utils/healthCheck.ts
================================================
/**
* Simple health check utility for the local Ito server using the main process
*/
export interface HealthCheckResult {
isHealthy: boolean
error?: string
}
/**
* Performs a health check against the local Ito server via the main process
* This avoids CORS issues by using the main process to make the HTTP request
* @returns Promise resolving to health check result
*/
export async function checkLocalServerHealth(): Promise {
try {
// Use the main process to check server health via HTTP
const result = await window.api.checkServerHealth()
return {
isHealthy: result.isHealthy,
error: result.error,
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred'
return {
isHealthy: false,
error: errorMessage,
}
}
}
================================================
FILE: app/utils/keyboard.test.ts
================================================
import { describe, test, expect, beforeEach, mock } from 'bun:test'
import { KeyState } from './keyboard'
import type { KeyEvent } from '@/lib/preload'
// Mock the window.api for KeyState tests
const mockApi = {
blockKeys: mock(),
}
global.window = {
api: mockApi as any,
} as any
beforeEach(() => {
mockApi.blockKeys.mockClear()
})
describe('KeyState', () => {
let keyState: KeyState
beforeEach(() => {
keyState = new KeyState()
})
describe('constructor', () => {
test('should initialize with empty shortcut by default', () => {
const state = new KeyState()
expect(state.getPressedKeys()).toEqual([])
})
test('should initialize with provided shortcut', () => {
const keyState = new KeyState(['command', 'space'])
expect(keyState).toBeDefined()
})
})
describe('updateShortcut', () => {
test('should update the shortcut', () => {
keyState.updateShortcut(['command', 'z'])
expect(keyState).toBeDefined()
})
test('should handle empty shortcut', () => {
keyState.updateShortcut([])
expect(keyState).toBeDefined()
})
})
describe('update', () => {
test('should track keydown events', () => {
keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)
expect(keyState.getPressedKeys()).toContain('a')
expect(keyState.isKeyPressed('a')).toBe(true)
})
test('should track keyup events', () => {
keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)
keyState.update({ key: 'KeyA', type: 'keyup' } as KeyEvent)
expect(keyState.getPressedKeys()).not.toContain('a')
expect(keyState.isKeyPressed('a')).toBe(false)
})
test('should ignore fn_fast events', () => {
keyState.update({ key: 'Unknown(179)', type: 'keydown' } as KeyEvent)
expect(keyState.getPressedKeys()).toEqual([])
})
test('should track multiple keys', () => {
keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)
keyState.update({ key: 'KeyB', type: 'keydown' } as KeyEvent)
expect(keyState.getPressedKeys()).toContain('a')
expect(keyState.getPressedKeys()).toContain('b')
expect(keyState.getPressedKeys()).toHaveLength(2)
})
})
describe('getPressedKeys', () => {
test('should return empty array initially', () => {
expect(keyState.getPressedKeys()).toEqual([])
})
test('should return currently pressed keys', () => {
keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)
keyState.update({ key: 'Space', type: 'keydown' } as KeyEvent)
const pressed = keyState.getPressedKeys()
expect(pressed).toContain('a')
expect(pressed).toContain('space')
expect(pressed).toHaveLength(2)
})
})
describe('isKeyPressed', () => {
test('should return false for unpressed keys', () => {
expect(keyState.isKeyPressed('a')).toBe(false)
})
test('should return true for pressed keys', () => {
keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)
expect(keyState.isKeyPressed('a')).toBe(true)
})
})
describe('clear', () => {
test('should clear all pressed keys', () => {
keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)
keyState.update({ key: 'KeyB', type: 'keydown' } as KeyEvent)
keyState.clear()
expect(keyState.getPressedKeys()).toEqual([])
expect(keyState.isKeyPressed('a')).toBe(false)
expect(keyState.isKeyPressed('b')).toBe(false)
})
test('should clear all pressed keys', () => {
keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)
keyState.clear()
expect(keyState.getPressedKeys()).toHaveLength(0)
})
})
describe('key blocking behavior', () => {
test('should track non-shortcut keys correctly', () => {
keyState.updateShortcut(['command', 'z'])
keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)
expect(keyState.getPressedKeys()).toContain('a')
})
test('should track keys when part of shortcut is pressed', () => {
keyState.updateShortcut(['command', 'z'])
keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)
expect(keyState.isKeyPressed('command-left')).toBe(true)
expect(keyState.getPressedKeys()).toContain('command-left')
})
test('should track keys when complete shortcut is pressed', () => {
keyState.updateShortcut(['command', 'z'])
keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)
keyState.update({ key: 'KeyZ', type: 'keydown' } as KeyEvent)
expect(keyState.isKeyPressed('command-left')).toBe(true)
expect(keyState.isKeyPressed('z')).toBe(true)
})
test('should track key releases correctly', () => {
keyState.updateShortcut(['command', 'z'])
keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)
keyState.update({ key: 'MetaLeft', type: 'keyup' } as KeyEvent)
expect(keyState.isKeyPressed('command-left')).toBe(false)
})
test('should handle complex shortcuts with multiple modifier keys', () => {
keyState.updateShortcut(['command', 'shift', 'z'])
keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)
keyState.update({ key: 'ShiftLeft', type: 'keydown' } as KeyEvent)
expect(keyState.isKeyPressed('command-left')).toBe(true)
expect(keyState.isKeyPressed('shift-left')).toBe(true)
})
test('should handle fn key in shortcuts', () => {
keyState.updateShortcut(['fn', 'f1'])
keyState.update({ key: 'Function', type: 'keydown' } as KeyEvent)
// Should track fn key presses
expect(keyState.isKeyPressed('fn')).toBe(true)
})
test('should track command keys correctly', () => {
keyState.updateShortcut(['command'])
keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)
// Should track the command key (as command-left)
expect(keyState.isKeyPressed('command-left')).toBe(true)
expect(keyState.getPressedKeys()).toContain('command-left')
})
})
describe('edge cases', () => {
test('should handle same key pressed multiple times', () => {
keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)
keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)
expect(keyState.getPressedKeys()).toEqual(['a'])
})
test('should handle keyup for unpressed key', () => {
keyState.update({ key: 'KeyA', type: 'keyup' } as KeyEvent)
expect(keyState.getPressedKeys()).toEqual([])
})
test('should handle shortcut change while keys are pressed', () => {
keyState.updateShortcut(['command', 'z'])
keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)
// Change shortcut while command is still pressed
keyState.updateShortcut(['command', 'x'])
// KeyState should still track the pressed key correctly
expect(keyState.isKeyPressed('command-left')).toBe(true)
})
})
})
================================================
FILE: app/utils/keyboard.ts
================================================
import { KeyEvent } from '@/lib/preload'
import { KeyboardShortcutConfig } from '@/lib/main/store'
import { ItoMode } from '../generated/ito_pb'
import {
keyNameMap,
normalizeLegacyKey,
getKeyDisplayInfo,
KeyName,
} from '@/lib/types/keyboard'
/**
* Helper to format directional indicators for modifier keys
*/
export function getDirectionalIndicator(
side: 'left' | 'right' | undefined,
showText: boolean = false,
): string {
if (!side) return ''
const arrow = side === 'left' ? '◀' : '▶'
if (showText) {
return side === 'left' ? `${arrow} left` : `right ${arrow}`
}
return arrow
}
/**
* Get formatted display components for a key
* @param keyboardKey The key name to display
* @param platform The platform to render keys for
* @param options Display options
* @returns Object with formatted display components
*/
export function getKeyDisplay(
keyboardKey: KeyName,
platform: 'darwin' | 'win32' | undefined = 'darwin',
options: {
showDirectionalText?: boolean
format?: 'symbol' | 'label' | 'both'
} = {},
): string {
const { showDirectionalText = false, format = 'symbol' } = options
const displayInfo = getKeyDisplayInfo(keyboardKey, platform)
const dirIndicator = getDirectionalIndicator(
displayInfo.side,
showDirectionalText,
)
const label = displayInfo.label
let result: string
if (displayInfo.isModifier && displayInfo.symbol) {
if (format === 'symbol') {
result = displayInfo.symbol
if (dirIndicator) {
result = showDirectionalText
? `${result} ${dirIndicator}`
: `${result} ${dirIndicator}`
}
} else if (format === 'label') {
result = label
if (dirIndicator) {
result = showDirectionalText
? `${result} ${dirIndicator}`
: `${result} ${dirIndicator}`
}
} else {
// 'both'
result = `${displayInfo.symbol} ${label}`
if (dirIndicator) {
result = `${result} ${dirIndicator}`
}
}
} else {
result = label
}
return result
}
export type ShortcutError =
| 'duplicate-key-same-mode'
| 'duplicate-key-diff-mode'
| 'not-found'
| 'reserved-combination'
export type ShortcutResult = {
success: boolean
error?: ShortcutError
errorMessage?: string
}
const MODIFIER_SEQUENCE = [
'control',
'control-left',
'control-right',
'option',
'option-left',
'option-right',
'alt',
'shift',
'shift-left',
'shift-right',
'command',
'command-left',
'command-right',
'fn',
] as const
const MODIFIER_INDEX: Record = MODIFIER_SEQUENCE.reduce(
(acc, key, i) => {
acc[key] = i
return acc
},
{} as Record