diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..cd8a4d65 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## Summary + + + +## Verification + + + +## Release status + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 867d71fa..4f7e8440 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: node-version: [20, 22] steps: - uses: actions/checkout@v6 + with: + fetch-depth: 0 - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} @@ -28,9 +30,17 @@ jobs: - run: npm run typecheck - run: npm run build:workspaces - run: npm test - - run: node --test scripts/release-gate.test.mjs scripts/sync-linear-coverage.test.mjs + - run: node --test scripts/release-gate.test.mjs scripts/native-release.test.mjs scripts/native-publish.test.mjs scripts/sync-linear-coverage.test.mjs + - run: node --test scripts/sdk-fixture-server.test.mjs scripts/multi-language-coverage.test.mjs examples/calls-example/server-credential.test.mjs + - run: python3 scripts/inspect-native-artifacts.test.py + - run: python3 scripts/prepare-php-mirror.test.py - run: npm run build + - run: npm run check:doc-snippets + - run: npm run test:doc-snippets + - run: npm run verify:doc-snippets -- typescript --negative-test + - run: npm run check:machine-credentials - run: npm run check:coverage + - run: node scripts/check-native-coverage.mjs - run: npm run check:functions - run: npm run check:names - run: npm pack --dry-run --workspaces --ignore-scripts diff --git a/.github/workflows/native-release.yml b/.github/workflows/native-release.yml new file mode 100644 index 00000000..4b4fc952 --- /dev/null +++ b/.github/workflows/native-release.yml @@ -0,0 +1,374 @@ +name: Prepare accepted native SDK releases + +on: + workflow_dispatch: + inputs: + channel: + description: Acceptance channel + type: choice + options: [nightly, stable] + default: nightly + version: + description: Exact x.y.z for stable; leave empty for nightly + type: string + +permissions: {} +concurrency: + group: native-sdk-release + cancel-in-progress: false + +jobs: + gate: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: ${{ inputs.channel == 'stable' && 'native-sdk-stable' || 'native-sdk-nightly' }} + permissions: + contents: read + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 24 + - id: acceptance-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + with: + client-id: ${{ secrets.SDK_COVERAGE_CLIENT_ID }} + private-key: ${{ secrets.SDK_COVERAGE_APP_PRIVATE_KEY }} + owner: polymorfa + repositories: polymorfa + permission-contents: read + permission-actions: read + - name: Require exact deployed API acceptance and complete native coverage + env: + GH_TOKEN: ${{ steps.acceptance-token.outputs.token }} + RELEASE_CHANNEL: ${{ inputs.channel }} + STABLE_VERSION: ${{ inputs.version }} + RELEASE_OUTPUT: .release/native + run: node scripts/native-release.mjs + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: accepted-native-contract + include-hidden-files: true + path: .release/native/release-contract.json + if-no-files-found: error + pack: + needs: gate + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + language: [go, python, php, rust, dotnet] + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 24 + - uses: actions/setup-go@v6 + if: matrix.language == 'go' + with: + go-version: "1.24.x" + cache: false + - uses: actions/setup-python@v6 + if: matrix.language == 'python' + with: + python-version: "3.10" + - if: matrix.language == 'python' + run: python -m pip install uv twine + - uses: shivammathur/setup-php@v2 + if: matrix.language == 'php' + with: + php-version: "8.2" + tools: composer:v2 + extensions: curl, openssl, mbstring + coverage: none + - uses: actions/setup-dotnet@v6 + if: matrix.language == 'dotnet' + with: + dotnet-version: "8.0.x" + - if: matrix.language == 'rust' + run: | + rustup toolchain install 1.85.0 --profile minimal --component rustfmt,clippy + rustup default 1.85.0 + rustup target add wasm32-unknown-unknown --toolchain 1.85.0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: accepted-native-contract + include-hidden-files: true + path: .release/native + - name: Build existing browser components for framework examples + run: | + npm ci + npm run build:workspaces + - name: Verify native package and examples + env: + SDK_LANGUAGE: ${{ matrix.language }} + SDK_PYTHON_VERSION: "3.10" + run: node scripts/verify-native-sdk.mjs "$SDK_LANGUAGE" + - name: Stamp registry versions and embedded provenance + env: + SDK_LANGUAGE: ${{ matrix.language }} + run: | + node scripts/stamp-native-release.mjs "$SDK_LANGUAGE" + node scripts/pack-native-release.mjs "$SDK_LANGUAGE" + - if: matrix.language == 'python' + run: python -m twine check .release/native/packs/python/*.whl .release/native/packs/python/*.tar.gz + - name: Prepare the accepted PHP root source mirror + if: matrix.language == 'php' + run: | + python3 scripts/prepare-php-mirror.py .release/native/packs/php .release/native/release-contract.json .release/native/php-mirror + composer validate --strict --no-check-publish .release/native/php-mirror/source/composer.json + - if: matrix.language == 'php' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-php-accepted-mirror + path: .release/native/php-mirror/ + if-no-files-found: error + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-${{ matrix.language }}-accepted-packs + include-hidden-files: true + path: .release/native/packs/${{ matrix.language }}/* + if-no-files-found: error + # Publication runs only after every accepted pack exists. Each registry job + # re-verifies the plan against the downloaded receipt and artifact hashes + # before any effect, refuses to overwrite a different existing version and + # stops dependents when a dependency is not confirmed. Environments hold the + # registry trust policies and required reviewers; see docs/native-release.md. + plan: + needs: pack + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 24 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: accepted-native-contract + include-hidden-files: true + path: .release/native + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: native-*-accepted-packs + path: .release/native/download + - name: Bind every accepted artifact to one publication plan + run: | + for language in go python php rust dotnet; do + mkdir -p ".release/native/packs/$language" + cp -a ".release/native/download/native-$language-accepted-packs/." ".release/native/packs/$language/" + python3 scripts/inspect-native-artifacts.py "$language" ".release/native/packs/$language" .release/native/release-contract.json + done + node scripts/native-publish.mjs plan .release/native/packs .release/native/release-contract.json .release/native/publish-plan.json + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-publish-plan + path: .release/native/publish-plan.json + if-no-files-found: error + publish-crates: + needs: [gate, plan] + runs-on: ubuntu-latest + environment: ${{ inputs.channel == 'stable' && 'native-sdk-stable' || 'native-sdk-nightly' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 24 + - run: rustup toolchain install 1.85.0 --profile minimal && rustup default 1.85.0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: accepted-native-contract + include-hidden-files: true + path: .release/native + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: native-publish-plan + path: .release/native + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: native-rust-accepted-packs + include-hidden-files: true + path: .release/native/packs/rust + - name: Exchange the OIDC token for a short-lived crates.io token + id: crates-auth + uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 + - name: Publish storage-core, then the SDK and IndexedDB crates + env: + CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }} + run: node scripts/native-publish.mjs publish crates .release/native/publish-plan.json + publish-nuget: + needs: [gate, plan] + runs-on: ubuntu-latest + environment: ${{ inputs.channel == 'stable' && 'native-sdk-stable' || 'native-sdk-nightly' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 24 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: accepted-native-contract + include-hidden-files: true + path: .release/native + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: native-publish-plan + path: .release/native + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: native-dotnet-accepted-packs + include-hidden-files: true + path: .release/native/packs/dotnet + - name: Exchange the OIDC token for a one-hour NuGet key + id: nuget-login + uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 + with: + user: ${{ vars.NUGET_USER }} + - name: Push Polymorfa.Sdk, then storage packages in dependency order + env: + NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} + run: node scripts/native-publish.mjs publish nuget .release/native/publish-plan.json + publish-pypi: + needs: [gate, plan] + runs-on: ubuntu-latest + environment: ${{ inputs.channel == 'stable' && 'native-sdk-stable' || 'native-sdk-nightly' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 24 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: accepted-native-contract + include-hidden-files: true + path: .release/native + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: native-publish-plan + path: .release/native + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: native-python-accepted-packs + include-hidden-files: true + path: .release/native/packs/python + - name: Refuse different existing files before uploading + run: node scripts/native-publish.mjs check pypi .release/native/publish-plan.json + - name: Isolate the accepted distributions + run: | + mkdir -p .release/native/pypi-dist + cp .release/native/packs/python/*.whl .release/native/packs/python/*.tar.gz .release/native/pypi-dist/ + - name: Upload through PyPI trusted publishing + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: .release/native/pypi-dist + skip-existing: true + - name: Confirm PyPI serves the accepted digests + run: node scripts/native-publish.mjs confirm pypi .release/native/publish-plan.json + publish-go: + needs: [gate, plan] + runs-on: ubuntu-latest + environment: ${{ inputs.channel == 'stable' && 'native-sdk-stable' || 'native-sdk-nightly' }} + # The only write: one new packages/go/v tag on this repository. + permissions: + contents: write + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + fetch-depth: 0 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 24 + - uses: actions/setup-go@v6 + with: + go-version: "1.24.x" + cache: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: accepted-native-contract + include-hidden-files: true + path: ${{ runner.temp }}/accepted + - name: Create the tag-only release commit and prove its module zip + run: node scripts/go-release-tag.mjs prepare "$RUNNER_TEMP/accepted/release-contract.json" "$RUNNER_TEMP/go-release.json" + - name: Push only the new tag + env: + GO_TAG_TOKEN: ${{ github.token }} + run: node scripts/go-release-tag.mjs push "$RUNNER_TEMP/go-release.json" + - name: Require the Go proxy to serve the same module hash + run: node scripts/go-release-tag.mjs verify "$RUNNER_TEMP/go-release.json" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-go-release + path: ${{ runner.temp }}/go-release.json + publish-php: + needs: [gate, plan] + runs-on: ubuntu-latest + environment: ${{ inputs.channel == 'stable' && 'native-sdk-stable' || 'native-sdk-nightly' }} + permissions: + contents: read + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 24 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: accepted-native-contract + include-hidden-files: true + path: .release/native + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: native-php-accepted-mirror + path: .release/native/php-mirror + - name: Require the dedicated mirror repository setting + env: + PHP_MIRROR_REPOSITORY: ${{ vars.PHP_MIRROR_REPOSITORY }} + run: test -n "$PHP_MIRROR_REPOSITORY" + - name: Mint a token for only the mirror repository + id: mirror-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + with: + client-id: ${{ vars.PHP_MIRROR_APP_CLIENT_ID }} + private-key: ${{ secrets.PHP_MIRROR_APP_PRIVATE_KEY }} + owner: polymorfa + repositories: ${{ vars.PHP_MIRROR_REPOSITORY }} + permission-contents: write + - name: Commit the accepted archive files to the mirror without forcing + id: mirror + env: + PHP_MIRROR_TOKEN: ${{ steps.mirror-token.outputs.token }} + PHP_MIRROR_REPOSITORY: ${{ vars.PHP_MIRROR_REPOSITORY }} + PHP_MIRROR_RECEIPT: .release/native/release-contract.json + run: | + node scripts/php-mirror-publish.mjs push .release/native/php-mirror "https://github.com/polymorfa/$PHP_MIRROR_REPOSITORY.git" > "$RUNNER_TEMP/php-mirror.json" + echo "commit=$(node -p 'JSON.parse(require("fs").readFileSync(process.argv[1])).commit' "$RUNNER_TEMP/php-mirror.json")" >> "$GITHUB_OUTPUT" + - name: Require Packagist to reference the mirror commit + env: + MIRROR_COMMIT: ${{ steps.mirror.outputs.commit }} + run: node scripts/php-mirror-publish.mjs verify .release/native/php-mirror "$MIRROR_COMMIT" diff --git a/.github/workflows/native-sdks.yml b/.github/workflows/native-sdks.yml new file mode 100644 index 00000000..3c69c5d6 --- /dev/null +++ b/.github/workflows/native-sdks.yml @@ -0,0 +1,122 @@ +name: Native server SDKs + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + maui-host: + runs-on: macos-26 + env: + DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.401 + - name: Install the supported Xcode 26.6 workload set + run: dotnet workload install maui-maccatalyst --version 10.0.401 + - name: Build existing browser components + run: | + npm ci + npm run build:workspaces + node packages/dotnet/examples/build-elements.mjs + - name: Build the native Blazor Hybrid application + run: dotnet build packages/dotnet/examples/maui/app/Polymorfa.Example.Maui.csproj -f net10.0-maccatalyst26.5 -c Debug -p:TreatWarningsAsErrors=true + - name: Verify the application authentication bridge + run: | + node --test packages/dotnet/examples/maui/bridge-tests/native-bridge.test.mjs + dotnet run --project packages/dotnet/examples/maui/bridge-tests/Polymorfa.Example.Maui.Bridge.Tests.csproj --configuration Release + matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.packages.outputs.matrix }} + enabled: ${{ steps.packages.outputs.enabled }} + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + - id: packages + run: node scripts/native-sdk-matrix.mjs >> "$GITHUB_OUTPUT" + + verify: + needs: matrix + if: needs.matrix.outputs.enabled == 'true' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: actions/setup-node@v6 + with: + node-version: 24 + - uses: actions/setup-go@v6 + if: matrix.language == 'go' + with: + go-version: ${{ matrix.runtime }} + cache: false + - uses: actions/setup-python@v6 + if: matrix.language == 'python' + with: + python-version: ${{ matrix.runtime }} + - name: Install Python tooling + if: matrix.language == 'python' + run: python -m pip install uv + - uses: shivammathur/setup-php@v2 + if: matrix.language == 'php' + with: + php-version: ${{ matrix.runtime }} + tools: composer:v2 + extensions: curl, openssl, mbstring + coverage: none + - uses: actions/setup-dotnet@v6 + if: matrix.language == 'dotnet' + with: + dotnet-version: ${{ matrix.runtime }} + - name: Install Rust toolchain + if: matrix.language == 'rust' + env: + RUST_TOOLCHAIN: ${{ matrix.runtime }} + run: | + rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal --component rustfmt,clippy + rustup default "$RUST_TOOLCHAIN" + rustup target add wasm32-unknown-unknown --toolchain "$RUST_TOOLCHAIN" + - name: Build existing browser components for framework examples + run: | + npm ci + npm run build:workspaces + - name: Check every exported documentation program and checker rejection + env: + SDK_LANGUAGE: ${{ matrix.language }} + SDK_PYTHON_VERSION: ${{ matrix.runtime }} + DOTNET_ROLL_FORWARD: Major + run: node scripts/verify-doc-snippets.mjs "$SDK_LANGUAGE" --negative-test + - name: Verify handwritten SDK and build its package + env: + SDK_LANGUAGE: ${{ matrix.language }} + SDK_PYTHON_VERSION: ${{ matrix.runtime }} + DOTNET_ROLL_FORWARD: Major + run: node scripts/verify-native-sdk.mjs "$SDK_LANGUAGE" + - name: Save native package artifacts + uses: actions/upload-artifact@v4 + with: + name: sdk-${{ matrix.language }}-${{ matrix.runtime }} + path: | + packages/python/dist/* + packages/dotnet/artifacts/*.nupkg + packages/rust/target/package/*.crate + packages/php/dist/*.tar + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 90d5e1cd..69c037ba 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,25 @@ coverage/ .DS_Store .release/ +packages/go/.artifacts/ +packages/python/.venv/ +packages/python/.mypy_cache/ +packages/python/.pytest_cache/ +packages/python/.ruff_cache/ +packages/python/**/__pycache__/ +packages/php/vendor/ +packages/php/artifacts/ +packages/php/.phpunit.cache/ +packages/rust/target/ +packages/dotnet/**/bin/ +packages/dotnet/**/obj/ +packages/dotnet/artifacts/ + +packages/*/release-contract.json +packages/python/src/polymorfa/release-contract.json +examples/**/__pycache__/ + +packages/dotnet/examples/aspnet-core/wwwroot/elements.js +packages/dotnet/examples/blazor-components/wwwroot/messaging.js + +packages/dotnet/examples/aspnet-core/wwwroot/blazor/ diff --git a/.prettierignore b/.prettierignore index 1d9d8670..6174c1f8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,24 @@ coverage/ contracts/openapi.messaging.json contracts/openapi.platform.json package-lock.json +packages/go/ +packages/python/ +packages/php/ +packages/rust/ +packages/dotnet/ +examples/**/vendor/ +examples/**/target/ +examples/**/assets/ +examples/**/dist/ +examples/**/bin/ +examples/**/obj/ +examples/rust/yew/frontend/public/elements.js +packages/typescript/src/history/gen/ +packages/typescript/proto/whatsapp.proto + +# Preserve the byte-identical API-owned signed-vector fixture. +contracts/machine-credentials.v1.json + +# Exact accepted API fragment bytes, verified by the Calls supplement checker. +contracts/calls-simultaneous-calls.fragments/messaging.json +contracts/calls-simultaneous-calls.fragments/platform.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..bb093dcc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,67 @@ +# SDK maintenance guide + +Start with `git status --short --branch` and preserve unrelated work. Inspect the +API contract, affected public SDK methods, callers, tests and existing PR before +editing. This repository's integration branch and PR base are `main`. + +## A feature counterpart covers every supported SDK + +When the user asks for an SDK counterpart to a feature, include TypeScript, Go, +Python, PHP, Rust and C# (.NET) in the same coordinated scope. Do not stop after +TypeScript or the language used by the first example. Implement sequentially in +dependency order unless the user explicitly authorizes another arrangement. + +For each affected language: + +- Update idiomatic public methods, typed requests/responses, validation, + serialization, errors, pagination, webhooks/events and examples as applicable. +- Verify the same API source revision, credential boundaries, idempotency, + cancellation and omission/null semantics through the language's native client. + A raw request escape hatch or a TypeScript transport does not count as a native + counterpart. +- Update `contracts/coverage.json` and the native package's `coverage.json` when + the accepted contract changes. Keep separately pinned supplement inventories + and their per-language wire evidence in sync. Do not mark unsupported methods + covered or advance a release pin just to make a check pass. +- Run the affected language's `scripts/verify-native-sdk.mjs` workflow and the + shared coverage checks. New or changed package surfaces also need a fresh + physical-package consumer; source imports do not prove package contents. +- Update affected language documentation and examples in this cycle. Coordinate + the customer docs and CLI against the same accepted dependency revision. + +Track every language as implemented, explicitly excluded with contract/policy +reason, or blocked by a named dependency in the internal handoff. Browser-only +renderers and media are not automatically server-SDK features. Investigate that +boundary and record any exclusion. Do not call the counterpart complete while an +in-scope language remains unfinished, and do not silently defer it to a later +request. Fixes to an existing feature must revisit all six implementations. + +Use `node scripts/check-native-coverage.mjs` and `npm run check:coverage` with the +matching contract revision. These check attribution and drift; passing them does +not replace native wire tests, package consumers or runtime acceptance. The API +drift workflow reports gaps; it does not generate or maintain handwritten SDKs. + +## Public PR descriptions + +Keep public SDK and CLI PR descriptions short. State the behavior, useful +verification and material release limitation. Link demos when relevant. Do not +paste feature-impact or no-impact tables, internal dependency inventories or +session transcripts into the description. Keep the detailed assessment in the +owning internal handoff and link the evidence when reviewers need it. Use the +small template in `.github/pull_request_template.md` for SDK PRs. + +Do not request automated reviews. Verify unsolicited findings against the current +source before acting. Keep implementation, local verification, CI, merge, +registry publication, deployment and customer availability as separate facts. +Nightlies require matching staging API acceptance; stable releases require +matching production acceptance. Never publish or claim installability from a +local package build alone. + +## Documentation snippet maintenance + +Public signature, request, export, package or coverage changes must revisit +`contracts/doc-snippets.json` and its explicit recipes. Follow +`contracts/doc-snippets.md`: bind the exact SDK source and API/spec inputs, +regenerate, check every emitted program with its actual native checker, and +retain cURL fallback for missing or mismatched entries. Source/package builds +are not registry publication or API acceptance evidence. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c51eca9..e903436f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ ## Unreleased +- Prepare simultaneous-call settings and Ban Insurance evidence in all six + server SDKs against accepted API #559 source `c1a43a1e`. Updates preserve + omission and explicit false. The API enforces acknowledgment and team-key + authority on an off-to-on transition. Claims and webhooks recognize + `simultaneous_calls`; claim evidence includes `simultaneousCalls`. + Package publication and deployed acceptance remain gated. +- Official API round-2 betas now have native source counterparts in all six + server SDKs: Number catalog-cache operations, marketing status and lifecycle + events, parent/previous identity fields, incoming carts, multipart Official + group pictures, Platform batching settings and whole-body signed batch + receivers. Graph resources and Linked Devices groups retain their own routes. + These changes are source-only. Exact combined-source hosted qualification, + accepted downstream source, API acceptance and publication remain required. + +- All six server SDKs submit each direct media grant, API relay upload, history + chunk retry and history media deletion at most once per invocation, including + when a caller supplies an idempotency key and a positive retry option. These + held routes have no server replay ledger. Errors retain their metadata; + established message-send/GET retries and direct CDN recovery are unchanged. + This correction is source-only and does not release the held API routes. + +- Project event streams in TypeScript, Go, Python, PHP and .NET retain their + resume cursor until the event payload validates. TypeScript now validates the + decoded webhook envelope. Their parsers enforce 4 MiB UTF-8 frame budgets + across incomplete lines, multiline data and comments. Rust's existing cursor + validation and frame bound gain regression coverage. Native malformed-payload + error and reconnect policies remain unchanged. +- Go and .NET storage synchronization preserve the configured initial cursor + when no checkpoint exists. A saved checkpoint still takes precedence; event + commit, checkpoint CAS and manual ACK retain their order. These repairs are + source-only and unreleased. + - Webhook phone offline start day. Platform and Messaging webhook create, update and read types gain `phoneOfflineStartDay` (7-13, default 10): the first day without the phone on which the webhook receives @@ -167,7 +199,7 @@ - Call consent. `Client.callPolicy` (`retrieve`, `update`) reads and replaces the team's blocked country codes with an optional `expectedRevision` guard, and `Client.callOptOuts` (`list`, `create`, `import`, `delete`) manages the - team's do-not-call list. Both need an organization key. `list` returns a + team's do-not-call list. Both need an team key. `list` returns a `CursorPage` that follows `page.nextCursor`; `create` answers `201` for a new entry and `200` for one already listed, told apart by `metadata.status`. `MessagingClient.voip.retrieveCallPermission()` reads a person's call @@ -195,7 +227,7 @@ - Added `Client.usage` to read monthly metered usage, page or iterate usage records, and inspect team usage gates. Project clients can read only their - own usage; gate state requires an organization key. The `usage.recorded` + own usage; gate state requires an team key. The `usage.recorded` webhook has a typed payload, and enforced limits use the `gate_limit_reached` error code. Usage is measured but not charged. @@ -719,7 +751,7 @@ incidents, claims, and Health action reads to `Client.banSafe`. Project and session resources now cover Safe Mode, warm-up, Ban Insurance evidence, and Health policy settings, and `MessagingClient.banSafe` covers - the same settings on the Messaging API for organization API keys and project + the same settings on the Messaging API for team keys and project tokens. Claim credit amounts are decimal quantities. Dashboard-only finding acknowledgement and appeals remain outside the server SDK. - Breaking: saved QuickLink settings drop `allowedRedirectUris` and add @@ -840,6 +872,18 @@ the `/api/voip/*` signaling routes and the `call.ended` / `call.telemetry` webhook events. +- Chat kit (`@polymorfa/browser`, `@polymorfa/ui`, `@polymorfa/react`, + `@polymorfa/elements`): search and media-type/direction filters over loaded + messages, received carts with reported unit prices and no computed total, + distinct not-retained/unavailable/deleted content states, a React + `ChatDetails` view, `inboxActionsFor()` row-action eligibility, Brazilian Portuguese chat and inbox strings, + `replies={false}` (`replies="false"`) for send paths that cannot quote, and + `createConversationSession(source, { sendUnavailable })`. + `` and `` link to a + QuickLink your server already created, with no client token or handler + call; `isQuickLinkUrl()` exposes the same URL check. Source-only and + unreleased. + ## 0.1.0-dev.0 - 2026-08-19 - Added handwritten Messaging clients for sessions, messages, and webhooks. diff --git a/README.md b/README.md index db18130b..eb9d1dd8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Handwritten API clients, UI packages, and developer tooling for Polymorfa. -The development branch contains the TypeScript server SDK, a framework-neutral +The repository contains the TypeScript server SDK, a framework-neutral browser runtime, shared UI contracts, Web Components, React bindings, thin Next.js server helpers, and a production-gated developer assistant. The exact Messaging and Platform sources are pinned in `contracts/source.json`; the Graph-compatible catalog, marketing status and @@ -34,29 +34,31 @@ The `@polymorfa/sdk` package root supports Node.js 20+, Cloudflare Workers, Deno, and Bun for HTTP clients and native webhook verification. Its `/node` entry contains Node.js file helpers. -The public packages are complete development artifacts on `dev`. They -publish to npm only as `dev` prereleases, never as `latest`. Their names are the intended public identities in the -Polymorfa npm organization. No mobile-native binding is part of this milestone. +SDK changes target `main`. Nightly prereleases use npm's `dev` tag after matching +staging acceptance; stable releases require matching production acceptance and +the protected release workflow. Source availability does not establish package +publication or feature availability. See [the release gates](docs/releasing.md). For a small browser preview against staging, see the [Calls example](examples/calls-example/README.md). ## TypeScript development install -Each push to `dev` publishes the public packages to npm under the `dev` -dist-tag, with versions such as `0.1.0-dev.20260919094454`: +The daily workflow checks `main` at 02:00 UTC and publishes eligible nightly +packages under the `dev` dist-tag, with versions such as +`0.1.0-dev.20260919094454`. Missing or mismatched staging acceptance blocks +publication: ```bash npm install @polymorfa/sdk@dev npm install @polymorfa/browser@dev # browser apps ``` -Pin an exact `0.1.0-dev.` version for reproducible installs. No -stable (`latest`) release exists. Publishing starts once the npm scope and -trusted publisher are configured; until `npm view @polymorfa/sdk dist-tags` -shows a `dev` tag, build from source and install the packed tarballs: +Pin an exact version for reproducible installs. Check +`npm view @polymorfa/sdk dist-tags` for published channels. To use unpublished +changes, check out their exact SDK commit and build packed tarballs: ```bash -git clone --branch dev https://github.com/polymorfa/sdks.git +git clone --branch main https://github.com/polymorfa/sdks.git cd sdks npm ci npm run build:workspaces @@ -67,7 +69,7 @@ npm install /path/to/sdks/polymorfa-sdk-0.1.0-dev.0.tgz # from your applicatio See [docs/releasing.md](docs/releasing.md) for the version scheme and the publishing workflow. -`npm install github:polymorfa/sdks#dev` no longer installs the SDK: the +`npm install github:polymorfa/sdks#main` does not install the SDK: the repository root is a private workspace, and `@polymorfa/sdk` lives in `packages/typescript`. @@ -134,7 +136,7 @@ The handwritten Messaging resources in this milestone are: - `banSafe`: retrieve and update project Safe Mode, warm-up, Ban Insurance evidence, and Health policy settings, and one number's Safe Mode override, - with an organization API key or project token + with an team key or project token - `sessions`: list, retrieve, update, delete, start, stop, restart, logout, account, and entitlement-gated direct JSON QR or phone pairing - `quickLinks`: create, retrieve, and cancel hosted QuickLink pairing sessions @@ -216,7 +218,7 @@ console.log(indexed.highWatermark, indexed.nextOffset); `Client` binds its ownership context when you construct it. An organization API key without `projectId` creates an organization client. Call `client.project(projectId)` to create an immutable project view, or construct a -project view directly with an organization key or a `pmfa_pt_` project token: +project view directly with an team key or a `pmfa_pt_` project token: ```ts const project = new Client({ @@ -278,7 +280,7 @@ project clients see only their project. The organization view also exposes these management resources: - `organizations`: retrieve the organization visible to the API key -- `apiKeys`: list key metadata and deactivate an organization API key +- `apiKeys`: list key metadata and deactivate an team key - `members`: list organization members - `auditLogs`: list the organization audit trail with live action, resource, and limit filters @@ -473,7 +475,7 @@ request ID and the W3C trace ID returned in `X-Trace-Id`. See ## Call consent Polymorfa checks every call against the team's call policy before the -destination rings. The policy is team-wide and needs an organization key; +destination rings. The policy is team-wide and needs an team key; project tokens and client tokens receive `403`. ```ts @@ -765,7 +767,7 @@ used by `Client`, `MessagingClient`, or their raw request helpers. `MessagingClient.quickLinks.create()`, `retrieve()`, and `cancel()` map the authenticated hosted lifecycle at `/messaging/quicklinks`. They accept organization API keys or project tokens with `quicklink:manage`; browser client tokens fail -before transport. Organization keys can set `projectId` on creation, while a +before transport. Team keys can set `projectId` on creation, while a project token remains bound by the server. These methods expose the short-lived connection URL and status record. They do @@ -865,6 +867,22 @@ original Customer action set. `chats_read`, `chats_manage`, `contact_write`, and `contact_delete` are reserved for enrolled regional beta access. These types do not establish regional availability or enable access by themselves. +An explicit empty session list seals zero actions. Keep it distinct from +omission when constructing the server-side mint request: + +| SDK | Omit the session ceiling | Seal zero session actions | +| ---------- | ----------------------------------------------------- | ------------------------- | +| TypeScript | Omit `allow` | `allow: []` | +| Go | `Allow: nil` | `Allow: []string{}` | +| Python | Omit the `allow` key | `"allow": []` | +| PHP | Omit the `allow` key | `'allow' => []` | +| Rust | `ClientTokenTarget::Session { session, allow: None }` | Set `allow: Some(vec![])` | +| C# | Omit `Allow` or use `null` | `Allow: []` | + +Rust uses `SessionClientAction` for session ceilings and `CustomerClientAction` +for Customer ceilings. Customer lists remain non-empty. The API validates +actions, enrollment and live rules; these request types grant no authority. + ### Calls `createBrowserCalls` connects the shared `@polymorfa/sdk/calls` model to the @@ -1051,3 +1069,5 @@ or Number authority. Writes require the exact `expectedRevision`, `prefer`, and ## License MIT + +Treat every Polymorfa credential as an opaque string. Never parse it or assume a fixed length. Credential storage must support at least 512 characters, preferably unrestricted text. Client-token length varies with its bindings and Customer scope. diff --git a/contracts/README.md b/contracts/README.md index 32a341e3..c83e1f77 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,5 +1,19 @@ # Contract coverage +## Campaign and custody additions + +`campaign-custody-20261014.json` pins thirty machine-accessible operations and +their referenced components to API source +`f97ad78b172e80788c33bdc0fcf28f75db9283ec`. TypeScript's new typed helpers select +`2026-10-14` when request options omit a version. The global API default and +579-operation baseline snapshots remain unchanged. + +The supplement is held with `published: false`, and the release gate checks +both source audiences. The five native ports, remaining 625-operation contract +reconciliation, accepted staging/production API evidence, and publication are +separate unfinished gates. `campaign-20261014-contract.test.ts` checks all thirty +wire operations; it does not establish provider delivery or deployed access. + ## Request log supplement `request-logs.json` pins `GET /platform/projects/{projectId}/request-logs` and @@ -75,8 +89,12 @@ compares their paths and fields. The full snapshots are not repinned here. The current snapshots are byte-identical copies of the Messaging and Platform contracts at API dev merge commit `171a9683ff45d11a4b5d3917dc428ad2722b0d99`. The full revision contains the inherited Logs and Usage contracts and all four -public Analytics reads. The ledger contains 579 operations: 433 covered and -146 excluded, with no missing, partial or changed fingerprints. Six reviewed +public Analytics reads. Each language ledger contains 579 operations: 442 +covered and 137 excluded, with no missing, partial or changed fingerprints +against these pinned snapshots. The three campaign update/reschedule methods +are verified in the TypeScript, Python and Rust wire suites; their old PR-owner +exclusions are removed. This baseline does not establish parity with newer API +contracts or package publication. Six reviewed Console changes cover analytics projections, consent preferences, operational error vitals and shared drains. They require dashboard identity and remain excluded from server SDK authority. @@ -120,7 +138,7 @@ public operations retain their typed SDK methods and unchanged fingerprints. Usage insights require dashboard identity; no server SDK method grants access to that Console route. The three Console-only setup operations also remain excluded from SDK authority. -Reads require organization credentials with `billing:read`; writes require +Reads require team keys with `billing:read`; writes require `billing:manage`, live team authority and Pay-As-You-Go. Existing credential masks do not gain these permissions. Paid service continues through its window when a limit is reached. The older Graph and test-event supplements keep their independent @@ -195,7 +213,7 @@ participants add optional `handRaised`. moving a launched campaign that is waiting to start. The Messaging update is sent once after an uncertain response because its route has no replay key; reschedule uses the API's idempotency key. The Platform reschedule request names -`projectId` for an organization credential. The other refreshed operation +`projectId` for an team key. The other refreshed operation fingerprints were checked against their existing method paths and types. files at API `dev` merge commit `e818ba62830d3727a7822379bb46c1fece90ef1b` (API PR #433, which corrects the payment order amount and status schemas from @@ -300,7 +318,7 @@ all 15 Functions routes already merged to API `dev`. HMS history adds four server-only Messaging reads under `MessagingClient.chats`: `list`, `retrieve`, `listMessages`, and -`retrieveMessage`. All four are covered; they require an organization key or +`retrieveMessage`. All four are covered; they require an team key or project token, their respective `chats:read` or `messages:read` scope, HMS on the Number, and `messaging.history` beta enrollment. Client tokens are refused locally. Pagination retains both cursors and the data-region response header. @@ -335,7 +353,7 @@ object. No routes were added or removed by this schema refresh. This revision also covers `GET /platform/usage`, `/platform/usage/records`, and `/platform/gates` through `Client.usage.summary`, `listRecords`, and `listGates`. `iterateRecords` follows record-page cursors. Usage is measured -but not charged; gate state requires an organization credential. +but not charged; gate state requires an team key. The Campaigns P0 revision added these operations. Audiences gain member management (`POST`/`GET /platform/audiences/{listId}/members` and @@ -643,9 +661,14 @@ settled skips. `contracts/campaign-frequency-cap.json` pins the merged API sourc package publication and API availability remain separate release gates. `campaign-segments-drip.json` pins the merged audience segment and drip additions -at API dev `f46ebd5162453e812dafa76ce02b420ddabe6921`, including source hashes, -focused schemas, fields and operations. The full baseline snapshots keep their -independent pins. This file records source adoption, not release availability. +at accepted API source `9bb1e2ed92d8b4a07ff727f8e7ae185498f0868f`, including +source hashes, focused schemas, fields and operations. The frequency-cap +supplement uses that same source. All 21 frequency-cap and 48 segment/drip +fragments are exactly unchanged at this revision. Their native campaign manifest +references match the two full source hashes. The full baseline snapshots and +mixed campaign/custody supplement keep their independent pins. This reconciliation +records source provenance; package publication, deployed acceptance and release +holds remain separate. ## Client-token ceilings @@ -658,3 +681,41 @@ Session `allow` requires the action-ceilings beta; an empty session list seals zero actions, while Customer lists remain non-empty. Reserved regional actions remain gated. The Next.js handler forwards only the grant's API actions and does not turn a wrapper-only grant into the session's broader API action set. + +All five native mint helpers preserve omitted, empty and populated session +ceilings. Go uses the minimum Go 1.24 `omitzero` encoding to distinguish nil from +an explicit empty slice. Python, PHP and Rust expose the reserved regional +action spellings in their request types; Go and C# retain their existing string +lists. Server-only mint guards remain in every language. This extends the +existing mint request, with no added operation or coverage-ledger claim. + +## Verified documentation snippets + +[`doc-snippets.json`](doc-snippets.json) exports checked programs for all six +server SDKs. Read [the consumer and maintenance contract](doc-snippets.md) before +using it: pin the export commit, match API/spec provenance, preserve exact +placeholders and key scope, and use cURL for absent or mismatched entries. +Current metadata describes source packages and omits registry install commands. + +## Machine-credential fixture supplement + +`machine-credentials.v1.json` is the byte-identical API #647 fixture from merged +source `e95d02da674da8242453c2a41efd48883fa04734`. Its producer path, blob and hash +are in `machine-credentials.source.json`. `npm run check:machine-credentials` +checks provenance and shared consumption in all six languages. Native credential +tests check framing, growth, unchanged wire transport and diagnostic redaction. +Signed negative-claim vectors are locally accepted as opaque strings; the API +authenticates them. This supplement does not repin the baseline OpenAPI snapshots, +change scopes, or establish package publication or deployed acceptance. + +## Simultaneous calls supplement + +`calls-simultaneous-calls.json` pins the exact GET/PUT call-settings operations, +request/response schemas and Ban Insurance claim/webhook changes to accepted +API #559 merge `c1a43a1eb409ad57c2b9677c606a3d42d7b5598c`. It records full +source hashes and canonical fragment hashes for each audience. +`native-calls-simultaneous-calls.json` attributes the six native counterparts +and tests. These are scoped supplements; the global `171a9683` baseline is +unchanged. `published: false` retains the release hold. Accepted API source +alone does not prove deployment, registry publication or installed-client +acceptance. PHP and Rust qualification remains pending. diff --git a/contracts/calls-simultaneous-calls.fragments/messaging.json b/contracts/calls-simultaneous-calls.fragments/messaging.json new file mode 100644 index 00000000..764dca77 --- /dev/null +++ b/contracts/calls-simultaneous-calls.fragments/messaging.json @@ -0,0 +1 @@ +{"operations":{},"schemas":{"BanSafeClaimPayload":{"additionalProperties":false,"properties":{"amountCents":{"description":"Credit quantity this claim returns, with up to six decimal places.","example":1080,"minimum":0,"multipleOf":1e-06,"type":"number"},"capCents":{"description":"Maximum credit quantity this claim can return, with up to six decimal places.","example":1080,"minimum":0,"multipleOf":1e-06,"type":"number"},"decidedAt":{"description":"ISO 8601 timestamp of the decision, or null while none was taken.","example":null,"format":"date-time","nullable":true,"type":"string"},"id":{"description":"Claim id. Stable and quotable.","example":"01936b2e-5f2a-7c1d-9a3e-8b4c2d1e0f22","type":"string"},"incidentId":{"description":"The incident this claim was filed against.","example":"01936b2e-5f2a-7c1d-9a3e-8b4c2d1e0f11","type":"string"},"measuredCents":{"description":"Credit quantity this number consumed inside the window, with up to six decimal places.","example":1080,"minimum":0,"multipleOf":1e-06,"type":"number"},"paidAt":{"description":"ISO 8601 timestamp the credits landed, or null.","example":null,"format":"date-time","nullable":true,"type":"string"},"phoneNumber":{"description":"The customer's own number in E.164 format.","example":"+15551234567","pattern":"^\\+[1-9]\\d{1,14}$","type":"string"},"reason":{"description":"One sentence describing what the ban was attributed to.","type":"string"},"status":{"description":"Where the claim sits. filed and under_review are open. approved means it was accepted, and with a zero amount that there was nothing to return. paid means the credits are on your balance. reversed means what Polymorfa knew about the ban changed afterwards and the credits were taken back.","enum":["filed","under_review","approved","denied","paid","reversed"],"type":"string"},"summary":{"description":"One sentence describing the claim's state.","type":"string"},"verdict":{"description":"What the ban was attributed to. other_device: another device on the account was sending. customer_conduct: Polymorfa was already restricting this number, or named the same serious problem for a week. shared_network: the number shared its connection with numbers that were being restricted. ours: nothing points at how the number was used. inconclusive: Polymorfa could not see enough of the window to say. simultaneous_calls: simultaneous calls were on for the number inside the window, which voids Ban Insurance; the claim is denied when filed.","enum":["other_device","customer_conduct","shared_network","ours","inconclusive","simultaneous_calls"],"type":"string"},"windowEnd":{"description":"ISO 8601 end of the window: the moment the ban started.","example":"2026-09-06T14:03:12.000Z","format":"date-time","type":"string"},"windowStart":{"description":"ISO 8601 start of the 30 days the refund covers.","example":"2026-08-07T14:03:12.000Z","format":"date-time","type":"string"}},"required":["id","incidentId","phoneNumber","status","verdict","windowStart","windowEnd","measuredCents","capCents","amountCents","summary","reason","decidedAt","paidAt"],"type":"object"}}} \ No newline at end of file diff --git a/contracts/calls-simultaneous-calls.fragments/platform.json b/contracts/calls-simultaneous-calls.fragments/platform.json new file mode 100644 index 00000000..fc2b2e4e --- /dev/null +++ b/contracts/calls-simultaneous-calls.fragments/platform.json @@ -0,0 +1 @@ +{"operations":{"/platform/sessions/{session}/call-settings":{"get":{"description":"**Required scope:** `sessions:read`","operationId":"getCallSettings","parameters":[{"in":"path","name":"session","required":true,"schema":{"maxLength":128,"minLength":1,"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformAccessGetCallSettingsResponse"}}},"description":"Call settings. Sessions without saved settings return the defaults.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"400":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#invalid-parameter","error":{"code":"invalid_parameter","message":"Correct the invalid request field.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"invalid_request_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"Bad request — request body or params failed validation","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"401":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#invalid-credential","error":{"code":"invalid_credential","message":"Supply a valid credential.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"authentication_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"Missing or invalid authentication token","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"403":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#permission-denied","error":{"code":"permission_denied","message":"The credential does not have access to this resource.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"permission_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"Authenticated but not permitted (missing scope or rule violation)","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"404":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#resource-not-found","error":{"code":"resource_not_found","message":"The resource is unavailable to this caller.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"invalid_request_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"Resource not found","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"500":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#internal-error","error":{"code":"internal_error","message":"An unexpected service failure occurred. Keep the request ID for support.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"api_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"Internal server error","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}}},"security":[{"ApiKeyAuth":[]}],"summary":"Get call settings for a session","tags":["Calls"],"x-audience":"public","x-mint":{"content":"sessions:read"},"x-required-scope":"sessions:read"},"put":{"description":"**Required scope:** `sessions:manage`\n\nChanges the settings you send; omitted settings keep their current values. Send `expectedRevision` to refuse the update when someone else changed the settings after you read them.","operationId":"updateCallSettings","parameters":[{"in":"path","name":"session","required":true,"schema":{"maxLength":128,"minLength":1,"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformAccessUpdateCallSettingsRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformAccessUpdateCallSettingsResponse"}}},"description":"Call settings saved. Audio settings apply to a call the next time a participant answers or connects to it, including calls in progress. Routing settings apply to calls that arrive after the change.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"400":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#invalid-parameter","error":{"code":"invalid_parameter","message":"Correct the invalid request field.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"invalid_request_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"Bad request — request body or params failed validation","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"401":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#invalid-credential","error":{"code":"invalid_credential","message":"Supply a valid credential.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"authentication_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"Missing or invalid authentication token","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"403":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#permission-denied","error":{"code":"permission_denied","message":"The credential does not have access to this resource.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"permission_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"Authenticated but not permitted (missing scope or rule violation)","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"404":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#resource-not-found","error":{"code":"resource_not_found","message":"The resource is unavailable to this caller.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"invalid_request_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"Resource not found","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"409":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#state-conflict","error":{"code":"state_conflict","message":"The request conflicts with the resource state.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"conflict_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"`state_conflict`: the settings changed after `expectedRevision`.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"500":{"content":{"application/json":{"example":{"data":null,"docs":"https://docs.polymorfa.com/api/errors#internal-error","error":{"code":"internal_error","message":"An unexpected service failure occurred. Keep the request ID for support.","param":null,"request_id":"5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30","type":"api_error"}},"schema":{"$ref":"#/components/schemas/PlatformAccessPublicError"}}},"description":"Internal server error","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}}},"security":[{"ApiKeyAuth":[]}],"summary":"Update call settings for a session","tags":["Calls"],"x-audience":"public","x-mint":{"content":"sessions:manage"},"x-required-scope":"sessions:manage"}}},"schemas":{"BanSafeClaim":{"additionalProperties":false,"properties":{"amountCents":{"description":"Credit quantity this claim returns, with up to six decimal places.","minimum":0,"multipleOf":1e-06,"type":"number"},"capCents":{"description":"Maximum credit quantity this claim can return, with up to six decimal places.","minimum":0,"multipleOf":1e-06,"type":"number"},"createdAt":{"format":"date-time","type":"string"},"decidedAt":{"format":"date-time","nullable":true,"type":"string"},"evidence":{"$ref":"#/components/schemas/BanSafeClaimEvidence"},"id":{"format":"uuid","type":"string"},"incidentId":{"format":"uuid","type":"string"},"measuredCents":{"description":"Credit quantity this number consumed inside the window, with up to six decimal places.","minimum":0,"multipleOf":1e-06,"type":"number"},"paidAt":{"format":"date-time","nullable":true,"type":"string"},"phoneNumber":{"type":"string"},"projectId":{"format":"uuid","type":"string"},"reason":{"type":"string"},"session":{"type":"string"},"sessionId":{"format":"uuid","type":"string"},"status":{"enum":["filed","under_review","approved","denied","paid","reversed"],"type":"string"},"summary":{"type":"string"},"verdict":{"enum":["other_device","customer_conduct","shared_network","ours","inconclusive","simultaneous_calls"],"type":"string"},"windowEnd":{"format":"date-time","type":"string"},"windowStart":{"format":"date-time","type":"string"}},"required":["id","incidentId","sessionId","session","phoneNumber","projectId","status","verdict","windowStart","windowEnd","measuredCents","capCents","amountCents","evidence","summary","reason","decidedAt","paidAt","createdAt"],"type":"object"},"BanSafeClaimEvidence":{"additionalProperties":false,"description":"Counts behind the attribution. Never a recipient, a message, or a device model.","properties":{"attributionRuleVersion":{"description":"Attribution rules used when the claim was filed. Null for claims filed before rule versioning.","minimum":1,"nullable":true,"type":"integer"},"criticalFindingDays":{"description":"Days one safety check stayed at its most serious level inside the window.","minimum":0,"type":"integer"},"deviceEvidence":{"description":"Whether ban insurance evidence collection is on for the project.","type":"boolean"},"measuredHours":{"description":"Hours of measured activity Polymorfa holds for the window.","minimum":0,"type":"integer"},"otherDevices":{"minimum":0,"type":"integer"},"restrictedInWindow":{"description":"Whether Polymorfa was restricting this number's sending inside the window.","type":"boolean"},"sharedConnection":{"description":"Whether this number shared its connection with numbers that were being restricted.","type":"boolean"},"simultaneousCalls":{"description":"Whether simultaneous calls were on for this number inside the window. When true, Ban Insurance does not cover the ban and the claim is denied when filed.","type":"boolean"},"windowDays":{"minimum":1,"type":"integer"}},"required":["attributionRuleVersion","windowDays","deviceEvidence","otherDevices","restrictedInWindow","criticalFindingDays","sharedConnection","measuredHours","simultaneousCalls"],"type":"object"},"PlatformAccessCallSettings":{"additionalProperties":false,"properties":{"callsEnabled":{"description":"Whether the session can place, answer and receive calls. When false, placing, answering, joining and attaching media fail with `403 calls_disabled`, incoming calls are declined, and SIP trunks cannot place calls through the session. Calls in progress continue, and the participant that answered or placed one can reconnect to it. Defaults to true.","type":"boolean"},"conferenceMode":{"description":"Conference mode. When true, every participant you connect to a call (including SIP trunk callers) hears the WhatsApp party and every other participant you connected. When false, each of them hears only the WhatsApp party. The WhatsApp party always hears all of your participants, and no participant hears their own audio. Defaults to true.","type":"boolean"},"hostCloudApiCalls":{"description":"Whether Polymorfa Calls answers the session's incoming calls when the session uses the Cloud API. When false (the default), your app answers them through the Graph API `/calls` endpoint and Polymorfa Calls does not host them. Sessions on a linked device ignore it.","type":"boolean"},"inboundRoute":{"$ref":"#/components/schemas/PlatformAccessCallInboundRoute"},"revision":{"description":"Increases on every change; 0 while the session uses the defaults. Send it as `expectedRevision` to refuse an update that would overwrite someone else's change.","minimum":0,"type":"integer"},"simultaneousCalls":{"description":"Whether this Number may hold more than one call at a time, with no limit. Linked-device sessions only; Cloud API sessions ignore it. WhatsApp's own apps never hold two calls at once, so this behavior is unusual for WhatsApp, and it voids Ban Insurance: a ban whose 30-day window overlaps any period this setting was on is not refunded. Defaults to false.","type":"boolean"},"sipClaim":{"description":"Whether an answer from the SIP trunk claims the call, which ends ringing on your other clients. Defaults to true.","type":"boolean"},"sipTrunkId":{"description":"The SIP trunk that receives incoming calls when `inboundRoute` is `sip_trunk`; otherwise null.","format":"uuid","nullable":true,"type":"string"},"updatedAt":{"description":"When the settings were last changed, or null when the session uses the defaults.","format":"date-time","nullable":true,"type":"string"}},"required":["callsEnabled","conferenceMode","inboundRoute","sipTrunkId","sipClaim","hostCloudApiCalls","simultaneousCalls","revision","updatedAt"],"type":"object"},"PlatformAccessUpdateCallSettingsRequest":{"additionalProperties":false,"properties":{"acknowledgeRisk":{"description":"Required as true when the update turns `simultaneousCalls` on. Confirms that you accept the unusual-behavior risk and the loss of Ban Insurance for this Number.","type":"boolean"},"callsEnabled":{"description":"Set to false to refuse new and incoming calls on the session; true allows them again.","type":"boolean"},"conferenceMode":{"description":"Set to true so that the participants you connect to a call hear each other as well as the WhatsApp party, or false so that each hears only the WhatsApp party.","type":"boolean"},"expectedRevision":{"description":"Apply the update only if the settings still have this `revision` (0 for a session using the defaults). Otherwise the update fails with `409 state_conflict`.","minimum":0,"type":"integer"},"hostCloudApiCalls":{"description":"Set to true to have Polymorfa Calls answer the session's Cloud API calls instead of your Graph API integration. Applies to calls that arrive after the change.","type":"boolean"},"inboundRoute":{"allOf":[{"$ref":"#/components/schemas/PlatformAccessCallInboundRoute"},{"description":"Where incoming WhatsApp calls ring."}]},"simultaneousCalls":{"description":"Set to true to let this Number hold more than one call at a time, with no limit. Turning it on requires `acknowledgeRisk: true` and a team API key; it voids Ban Insurance for any ban whose 30-day window overlaps a period it was on. Set to false to return to one call at a time. Applies to calls that start after the change.","type":"boolean"},"sipClaim":{"description":"Whether an answer from the SIP trunk claims the call.","type":"boolean"},"sipTrunkId":{"description":"The SIP trunk for `sip_trunk`: a trunk in the session's project whose direction is `outbound` or `both`. Required when switching the route to `sip_trunk`; omit it to keep the stored trunk. With `clients` send `null` or omit it: a trunk ID is refused, and `clients` clears the stored trunk.","format":"uuid","nullable":true,"type":"string"}},"type":"object"}}} \ No newline at end of file diff --git a/contracts/calls-simultaneous-calls.json b/contracts/calls-simultaneous-calls.json new file mode 100644 index 00000000..9e4562f6 --- /dev/null +++ b/contracts/calls-simultaneous-calls.json @@ -0,0 +1,898 @@ +{ + "repository": "polymorfa/polymorfa", + "commit": "c1a43a1eb409ad57c2b9677c606a3d42d7b5598c", + "branch": "dev", + "pullRequest": "polymorfa/polymorfa#559", + "published": false, + "note": "Accepted API #559 merge source. Package publication, deployed acceptance and full baseline reconciliation remain separate gates. Acknowledgment is required by the API only for an off-to-on transition.", + "sources": { + "messaging": { + "sourcePath": "apps/api/docs/openapi.json", + "sourceSha256": "ca120160dc14d523c74e3772baa415ddc6ae5b857ddc40c4ed39479554e1e9b3", + "schemas": { + "BanSafeClaimPayload": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Claim id. Stable and quotable.", + "example": "01936b2e-5f2a-7c1d-9a3e-8b4c2d1e0f22" + }, + "incidentId": { + "type": "string", + "description": "The incident this claim was filed against.", + "example": "01936b2e-5f2a-7c1d-9a3e-8b4c2d1e0f11" + }, + "phoneNumber": { + "type": "string", + "pattern": "^\\+[1-9]\\d{1,14}$", + "description": "The customer's own number in E.164 format.", + "example": "+15551234567" + }, + "status": { + "type": "string", + "enum": [ + "filed", + "under_review", + "approved", + "denied", + "paid", + "reversed" + ], + "description": "Where the claim sits. filed and under_review are open. approved means it was accepted, and with a zero amount that there was nothing to return. paid means the credits are on your balance. reversed means what Polymorfa knew about the ban changed afterwards and the credits were taken back." + }, + "verdict": { + "type": "string", + "enum": [ + "other_device", + "customer_conduct", + "shared_network", + "ours", + "inconclusive", + "simultaneous_calls" + ], + "description": "What the ban was attributed to. other_device: another device on the account was sending. customer_conduct: Polymorfa was already restricting this number, or named the same serious problem for a week. shared_network: the number shared its connection with numbers that were being restricted. ours: nothing points at how the number was used. inconclusive: Polymorfa could not see enough of the window to say. simultaneous_calls: simultaneous calls were on for the number inside the window, which voids Ban Insurance; the claim is denied when filed." + }, + "windowStart": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 start of the 30 days the refund covers.", + "example": "2026-08-07T14:03:12.000Z" + }, + "windowEnd": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 end of the window: the moment the ban started.", + "example": "2026-09-06T14:03:12.000Z" + }, + "measuredCents": { + "type": "number", + "description": "Credit quantity this number consumed inside the window, with up to six decimal places.", + "minimum": 0, + "multipleOf": 1e-6, + "example": 1080 + }, + "capCents": { + "type": "number", + "description": "Maximum credit quantity this claim can return, with up to six decimal places.", + "minimum": 0, + "multipleOf": 1e-6, + "example": 1080 + }, + "amountCents": { + "type": "number", + "description": "Credit quantity this claim returns, with up to six decimal places.", + "minimum": 0, + "multipleOf": 1e-6, + "example": 1080 + }, + "summary": { + "type": "string", + "description": "One sentence describing the claim's state." + }, + "reason": { + "type": "string", + "description": "One sentence describing what the ban was attributed to." + }, + "decidedAt": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "ISO 8601 timestamp of the decision, or null while none was taken.", + "example": null + }, + "paidAt": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "ISO 8601 timestamp the credits landed, or null.", + "example": null + } + }, + "required": [ + "id", + "incidentId", + "phoneNumber", + "status", + "verdict", + "windowStart", + "windowEnd", + "measuredCents", + "capCents", + "amountCents", + "summary", + "reason", + "decidedAt", + "paidAt" + ], + "additionalProperties": false + } + }, + "fragmentSha256": "acc80776a3f8a0bd87aabb2715d69ffbe96a7e06884c3f10406d83a87b09f48f", + "fragmentPath": "contracts/calls-simultaneous-calls.fragments/messaging.json" + }, + "platform": { + "sourcePath": "apps/api/docs/openapi.management.json", + "sourceSha256": "7c591184bd062c69c27c1f3cb7d8b2b80333257e64cba5ca0af2c6c4045c0820", + "schemas": { + "PlatformAccessCallSettings": { + "type": "object", + "properties": { + "callsEnabled": { + "type": "boolean", + "description": "Whether the session can place, answer and receive calls. When false, placing, answering, joining and attaching media fail with `403 calls_disabled`, incoming calls are declined, and SIP trunks cannot place calls through the session. Calls in progress continue, and the participant that answered or placed one can reconnect to it. Defaults to true." + }, + "conferenceMode": { + "type": "boolean", + "description": "Conference mode. When true, every participant you connect to a call (including SIP trunk callers) hears the WhatsApp party and every other participant you connected. When false, each of them hears only the WhatsApp party. The WhatsApp party always hears all of your participants, and no participant hears their own audio. Defaults to true." + }, + "inboundRoute": { + "$ref": "#/components/schemas/PlatformAccessCallInboundRoute" + }, + "sipTrunkId": { + "type": "string", + "nullable": true, + "format": "uuid", + "description": "The SIP trunk that receives incoming calls when `inboundRoute` is `sip_trunk`; otherwise null." + }, + "sipClaim": { + "type": "boolean", + "description": "Whether an answer from the SIP trunk claims the call, which ends ringing on your other clients. Defaults to true." + }, + "hostCloudApiCalls": { + "type": "boolean", + "description": "Whether Polymorfa Calls answers the session's incoming calls when the session uses the Cloud API. When false (the default), your app answers them through the Graph API `/calls` endpoint and Polymorfa Calls does not host them. Sessions on a linked device ignore it." + }, + "simultaneousCalls": { + "type": "boolean", + "description": "Whether this Number may hold more than one call at a time, with no limit. Linked-device sessions only; Cloud API sessions ignore it. WhatsApp's own apps never hold two calls at once, so this behavior is unusual for WhatsApp, and it voids Ban Insurance: a ban whose 30-day window overlaps any period this setting was on is not refunded. Defaults to false." + }, + "revision": { + "type": "integer", + "minimum": 0, + "description": "Increases on every change; 0 while the session uses the defaults. Send it as `expectedRevision` to refuse an update that would overwrite someone else's change." + }, + "updatedAt": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "When the settings were last changed, or null when the session uses the defaults." + } + }, + "required": [ + "callsEnabled", + "conferenceMode", + "inboundRoute", + "sipTrunkId", + "sipClaim", + "hostCloudApiCalls", + "simultaneousCalls", + "revision", + "updatedAt" + ], + "additionalProperties": false + }, + "PlatformAccessUpdateCallSettingsRequest": { + "type": "object", + "properties": { + "callsEnabled": { + "type": "boolean", + "description": "Set to false to refuse new and incoming calls on the session; true allows them again." + }, + "conferenceMode": { + "type": "boolean", + "description": "Set to true so that the participants you connect to a call hear each other as well as the WhatsApp party, or false so that each hears only the WhatsApp party." + }, + "inboundRoute": { + "allOf": [ + { + "$ref": "#/components/schemas/PlatformAccessCallInboundRoute" + }, + { + "description": "Where incoming WhatsApp calls ring." + } + ] + }, + "sipTrunkId": { + "type": "string", + "nullable": true, + "format": "uuid", + "description": "The SIP trunk for `sip_trunk`: a trunk in the session's project whose direction is `outbound` or `both`. Required when switching the route to `sip_trunk`; omit it to keep the stored trunk. With `clients` send `null` or omit it: a trunk ID is refused, and `clients` clears the stored trunk." + }, + "sipClaim": { + "type": "boolean", + "description": "Whether an answer from the SIP trunk claims the call." + }, + "hostCloudApiCalls": { + "type": "boolean", + "description": "Set to true to have Polymorfa Calls answer the session's Cloud API calls instead of your Graph API integration. Applies to calls that arrive after the change." + }, + "simultaneousCalls": { + "type": "boolean", + "description": "Set to true to let this Number hold more than one call at a time, with no limit. Turning it on requires `acknowledgeRisk: true` and a team API key; it voids Ban Insurance for any ban whose 30-day window overlaps a period it was on. Set to false to return to one call at a time. Applies to calls that start after the change." + }, + "acknowledgeRisk": { + "type": "boolean", + "description": "Required as true when the update turns `simultaneousCalls` on. Confirms that you accept the unusual-behavior risk and the loss of Ban Insurance for this Number." + }, + "expectedRevision": { + "type": "integer", + "minimum": 0, + "description": "Apply the update only if the settings still have this `revision` (0 for a session using the defaults). Otherwise the update fails with `409 state_conflict`." + } + }, + "additionalProperties": false + }, + "BanSafeClaim": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "incidentId": { + "type": "string", + "format": "uuid" + }, + "sessionId": { + "type": "string", + "format": "uuid" + }, + "session": { + "type": "string" + }, + "phoneNumber": { + "type": "string" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": [ + "filed", + "under_review", + "approved", + "denied", + "paid", + "reversed" + ] + }, + "verdict": { + "type": "string", + "enum": [ + "other_device", + "customer_conduct", + "shared_network", + "ours", + "inconclusive", + "simultaneous_calls" + ] + }, + "windowStart": { + "type": "string", + "format": "date-time" + }, + "windowEnd": { + "type": "string", + "format": "date-time" + }, + "measuredCents": { + "type": "number", + "minimum": 0, + "multipleOf": 1e-6, + "description": "Credit quantity this number consumed inside the window, with up to six decimal places." + }, + "capCents": { + "type": "number", + "minimum": 0, + "multipleOf": 1e-6, + "description": "Maximum credit quantity this claim can return, with up to six decimal places." + }, + "amountCents": { + "type": "number", + "minimum": 0, + "multipleOf": 1e-6, + "description": "Credit quantity this claim returns, with up to six decimal places." + }, + "evidence": { + "$ref": "#/components/schemas/BanSafeClaimEvidence" + }, + "summary": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "decidedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "paidAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "incidentId", + "sessionId", + "session", + "phoneNumber", + "projectId", + "status", + "verdict", + "windowStart", + "windowEnd", + "measuredCents", + "capCents", + "amountCents", + "evidence", + "summary", + "reason", + "decidedAt", + "paidAt", + "createdAt" + ] + }, + "BanSafeClaimEvidence": { + "type": "object", + "additionalProperties": false, + "description": "Counts behind the attribution. Never a recipient, a message, or a device model.", + "properties": { + "attributionRuleVersion": { + "type": "integer", + "minimum": 1, + "nullable": true, + "description": "Attribution rules used when the claim was filed. Null for claims filed before rule versioning." + }, + "windowDays": { + "type": "integer", + "minimum": 1 + }, + "deviceEvidence": { + "type": "boolean", + "description": "Whether ban insurance evidence collection is on for the project." + }, + "otherDevices": { + "type": "integer", + "minimum": 0 + }, + "restrictedInWindow": { + "type": "boolean", + "description": "Whether Polymorfa was restricting this number's sending inside the window." + }, + "criticalFindingDays": { + "type": "integer", + "minimum": 0, + "description": "Days one safety check stayed at its most serious level inside the window." + }, + "sharedConnection": { + "type": "boolean", + "description": "Whether this number shared its connection with numbers that were being restricted." + }, + "measuredHours": { + "type": "integer", + "minimum": 0, + "description": "Hours of measured activity Polymorfa holds for the window." + }, + "simultaneousCalls": { + "type": "boolean", + "description": "Whether simultaneous calls were on for this number inside the window. When true, Ban Insurance does not cover the ban and the claim is denied when filed." + } + }, + "required": [ + "attributionRuleVersion", + "windowDays", + "deviceEvidence", + "otherDevices", + "restrictedInWindow", + "criticalFindingDays", + "sharedConnection", + "measuredHours", + "simultaneousCalls" + ] + } + }, + "operations": { + "/platform/sessions/{session}/call-settings": { + "get": { + "operationId": "getCallSettings", + "tags": ["Calls"], + "summary": "Get call settings for a session", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-required-scope": "sessions:read", + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "required": true, + "name": "session", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Call settings. Sessions without saved settings return the defaults.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessGetCallSettingsResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request \u2014 request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "sessions:read" + }, + "description": "**Required scope:** `sessions:read`", + "x-audience": "public" + }, + "put": { + "operationId": "updateCallSettings", + "tags": ["Calls"], + "summary": "Update call settings for a session", + "description": "**Required scope:** `sessions:manage`\n\nChanges the settings you send; omitted settings keep their current values. Send `expectedRevision` to refuse the update when someone else changed the settings after you read them.", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-required-scope": "sessions:manage", + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "required": true, + "name": "session", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessUpdateCallSettingsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Call settings saved. Audio settings apply to a call the next time a participant answers or connects to it, including calls in progress. Routing settings apply to calls that arrive after the change.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessUpdateCallSettingsResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request \u2014 request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "409": { + "description": "`state_conflict`: the settings changed after `expectedRevision`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "conflict_error", + "code": "state_conflict", + "message": "The request conflicts with the resource state.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#state-conflict" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlatformAccessPublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "sessions:manage" + }, + "x-audience": "public" + } + } + }, + "fragmentSha256": "4a9a6a17fb4f4ad38a524030586ee8e939bdae0f1c8726d75bcaacc6699714c4", + "fragmentPath": "contracts/calls-simultaneous-calls.fragments/platform.json" + } + }, + "sourceAccepted": true +} diff --git a/contracts/campaign-custody-20261014.json b/contracts/campaign-custody-20261014.json new file mode 100644 index 00000000..a513a5eb --- /dev/null +++ b/contracts/campaign-custody-20261014.json @@ -0,0 +1,6608 @@ +{ + "repository": "polymorfa/polymorfa", + "commit": "f97ad78b172e80788c33bdc0fcf28f75db9283ec", + "published": false, + "apiVersion": "2026-10-14", + "note": "Thirty campaign, audience profile, STOP confirmation and custody-default operations. TypeScript adoption is in source; five native ports, full 625-operation reconciliation, deployed acceptance and publication remain required. Global snapshots and their coverage ledger remain unchanged.", + "sources": { + "messaging": { + "sourcePath": "apps/api/docs/openapi.json", + "sourceSha256": "009bf72f1c0bb65c6f2d168e804fdd50f60ab1394a5a06632391b87986a5fcb4", + "paths": { + "/messaging/projects/{projectSlug}/campaigns/{id}/links": { + "get": { + "operationId": "getProjectCampaignLinks", + "tags": ["Campaigns"], + "summary": "Get tracked link opens", + "description": "**Required scope:** `campaigns:read`\n\nBeta. Opens of each tracked link, in the campaign's link order. An open is a request to the short link: a person, or an app or security scanner previewing the link. It is not proof of a click. Requires the campaign link tracking beta (`403 feature_unavailable` otherwise).", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "campaigns:read", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCampaignLinksResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "campaigns:read" + } + } + }, + "/messaging/projects/{projectSlug}/campaigns/{id}/test-send": { + "post": { + "operationId": "testSendCampaign", + "tags": ["Campaigns"], + "summary": "Send a test of a draft campaign", + "description": "**Required scopes (all):** `campaigns:manage` · `campaigns:read`\n\nRequires campaigns:manage and campaigns:read because the test can use stored recipient samples. Sends one copy of a draft campaign's content to a seed recipient, through the same rendering, sending, and BanSafe checks as a launched campaign. The seed recipient must be a number connected to your team. A test is not a campaign recipient and is not counted in the campaign's statistics or webhooks. A team can send 20 tests an hour, and at most 5 an hour to one seed recipient.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scopes": ["campaigns:manage", "campaigns:read"], + "x-required-scope-mode": "all", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "description": "Unique key, 1 to 255 bytes of UTF-8, that makes this write safe to retry for 24 hours. A retry with the same key, credential, `Polymorfa-Version`, and request never runs the write again: it returns `409 idempotency_completed` when the original succeeded, or the original server error, with `Idempotent-Replayed: true`. Reusing the key for a different request returns `409 idempotency_conflict`; a retry while the original is still running returns `409 idempotency_in_progress`, and a retry after an unrecorded outcome returns `409 idempotency_outcome_unknown`.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignTestSendRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestSendCampaignResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "402": { + "description": "`campaigns_not_entitled`: the sending number's plan does not include Campaigns.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "campaigns_not_entitled", + "message": "This number’s plan does not include Campaigns.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#campaigns-not-entitled" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "409": { + "description": "`state_conflict`: The campaign is not a draft or has A/B or weighted content alternatives, the sending number is a Hybrid Link number, or the seed recipient opted out. `bansafe_cold_held`: the seed recipient has never written to the sending number and the campaign holds first messages. The Idempotency-Key already succeeded (`idempotency_completed`), was used for a different request (`idempotency_conflict`), belongs to a request that is still running (`idempotency_in_progress`, with Retry-After), or belongs to a request whose outcome was not recorded (`idempotency_outcome_unknown`)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "conflict_error", + "code": "state_conflict", + "message": "The request conflicts with the resource state.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#state-conflict" + } + } + }, + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying `idempotency_in_progress`.", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + "Idempotent-Replayed": { + "description": "`true` when this response reports the recorded outcome of an earlier request with the same Idempotency-Key.", + "schema": { + "type": "string", + "enum": ["true"] + } + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "422": { + "description": "`campaign_variables_missing`: the selected sample and explicit values cannot fill a variable in the content.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "campaign_variables_missing", + "message": "A recipient has no value and no fallback for a variable in the campaign content.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#campaign-variables-missing" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "429": { + "description": "The team or seed recipient reached its test-send limit (`rate_limit_exceeded`), or BanSafe is pacing the sending number. Follow `Retry-After`.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "rate_limit_error", + "code": "rate_limit_exceeded", + "message": "The request limit was reached. Follow Retry-After when supplied.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#rate-limit-exceeded" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "502": { + "description": "`upstream_failure`: the send path reported that the test could not be sent.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "upstream_error", + "code": "upstream_failure", + "message": "The messaging provider could not complete the request.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#upstream-failure" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "503": { + "description": "The test was not sent: the sending number is not connected (`session_not_ready`), test sends or the send path are unavailable (`service_unavailable`, `command_dispatch_failed`), or the sending number's allowance could not be checked (`bansafe_accounting_unavailable`). Retry later.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "service_unavailable", + "message": "A required service is unavailable. Check completion before repeating a write.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#service-unavailable" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "504": { + "description": "`result_unknown`: the outcome was not received in time. The test may still arrive.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "result_unknown", + "message": "The result was not obtained before the deadline. Check completion before repeating a write.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#result-unknown" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "campaigns:manage campaigns:read" + } + } + }, + "/messaging/projects/{projectSlug}/campaign-reply-flows": { + "post": { + "operationId": "createCampaignReplyFlow", + "tags": ["Campaigns"], + "summary": "Create a reply flow revision", + "description": "**Required scope:** `campaigns:manage`\n\nCreates an immutable reply flow revision: the steps a campaign runs when a recipient replies. Each step can send a text, reply buttons, a list, or a Flow form, and branches on the button or list row chosen, an exact keyword, or a Flow form submission. Beta.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "campaigns:manage", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "projectSlug", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCampaignReplyFlowRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCampaignReplyFlowResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "`missing_scope`, or `feature_unavailable` when campaign reply flows are not enabled for the team.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "missing_scope", + "message": "The credential lacks a required API scope.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#missing-scope" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "409": { + "description": "`state_conflict`: this flow key and revision already exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "conflict_error", + "code": "state_conflict", + "message": "The request conflicts with the resource state.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#state-conflict" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "campaigns:manage" + } + } + }, + "/messaging/projects/{projectSlug}/campaign-reply-flows/{id}": { + "get": { + "operationId": "getCampaignReplyFlow", + "tags": ["Campaigns"], + "summary": "Get a reply flow revision", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "campaigns:read", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCampaignReplyFlowResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "`missing_scope`, or `feature_unavailable` when campaign reply flows are not enabled for the team.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "missing_scope", + "message": "The credential lacks a required API scope.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#missing-scope" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "campaigns:read" + }, + "description": "**Required scope:** `campaigns:read`" + } + }, + "/messaging/projects/{projectSlug}/campaigns/{id}/reply-flow": { + "put": { + "operationId": "setCampaignReplyFlow", + "tags": ["Campaigns"], + "summary": "Set a campaign's reply flow", + "description": "**Required scope:** `campaigns:manage`\n\nAttaches a reply flow revision to a draft or scheduled campaign, or detaches it with `null`. The flow starts for each recipient once their message is sent.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "campaigns:manage", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetCampaignReplyFlowRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetCampaignReplyFlowResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "`missing_scope`, or `feature_unavailable` when campaign reply flows are not enabled for the team.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "missing_scope", + "message": "The credential lacks a required API scope.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#missing-scope" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "409": { + "description": "`state_conflict`: the campaign has already launched.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "conflict_error", + "code": "state_conflict", + "message": "The request conflicts with the resource state.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#state-conflict" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "campaigns:manage" + } + } + }, + "/messaging/projects/{projectSlug}/campaigns/{id}/recipients/export": { + "get": { + "operationId": "exportProjectCampaignRecipients", + "tags": ["Campaigns"], + "summary": "Export campaign recipients", + "description": "**Required scope:** `campaigns:read`\n\nReturns recipients as CSV in queue order. Unfiltered exports page up to 1,000 recipients per request. Each page starts with a header row. When more recipients match, the `Polymorfa-Next-Cursor` response header carries the cursor for the next page; it is absent on the last page. Each page is read when it is requested, so a recipient appears once, with the outcome it had when its page was read. A filtered export is one atomic page of up to 1,000 recipients; larger filtered results return 409, so changing statuses cannot silently omit matches between pages. The export contains outcomes and `failureReason` codes only, never message content or template variables.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "campaigns:read", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "enum": [ + "queued", + "sending", + "sent", + "delivered", + "read", + "failed", + "skipped" + ] + }, + "required": false, + "name": "status", + "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "opted_out", + "cold_held", + "campaign_cancelled", + "invalid_recipient", + "session_not_connected", + "ack_timeout", + "blocked_by_safety", + "send_failed", + "missing_variable", + "other" + ], + "description": "Only recipients whose `failureReason` is this code." + }, + "required": false, + "name": "reason", + "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "The `Polymorfa-Next-Cursor` value from the previous unfiltered page. Filtered exports cannot be paged." + }, + "required": false, + "name": "cursor", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "description": "Recipients per unfiltered page, up to 1,000. Filtered exports require 1,000. Default 1,000." + }, + "required": false, + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "One page of recipients.", + "headers": { + "Polymorfa-Next-Cursor": { + "schema": { + "type": "string", + "description": "Cursor for the next page. Absent on the last page." + }, + "required": false + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "text/csv": { + "schema": { + "type": "string", + "description": "Header row, then one row per recipient. Columns: recipientId, phone, status, failureReason, attempts, variantKey, queuedAt, sentAt, deliveredAt, readAt, failedAt, respondedAt, followUpStatus, followUpAnchor, followUpDueAt, followUpExpiresAt, followUpSentAt, followUpReadAt, followUpRepliedAt, followUpReason, followUpCommandId. Times are ISO 8601 in UTC. Empty fields are null. Text that begins with =, +, -, @, tab or carriage return is prefixed with an apostrophe, including phone numbers in international format. Machine readers should remove that leading apostrophe from phone cells." + } + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "409": { + "description": "More than 1,000 recipients match the filter", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "conflict_error", + "code": "state_conflict", + "message": "The request conflicts with the resource state.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#state-conflict" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "campaigns:read" + } + } + } + }, + "components": { + "schemas": { + "GetCampaignLinksResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CampaignTrackedLinkReport" + } + } + }, + "required": ["success", "data"] + }, + "CampaignTrackedLinkReport": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Tracked link key.", + "example": "offer" + }, + "url": { + "type": "string", + "description": "Destination of the tracked link.", + "example": "https://example.com/spring-sale" + }, + "uniqueOpens": { + "type": "integer", + "minimum": 0, + "description": "Recipients who opened the link at least once." + }, + "totalOpens": { + "type": "integer", + "minimum": 0, + "description": "Every recorded open, including repeat opens." + } + }, + "required": ["key", "url", "uniqueOpens", "totalOpens"] + }, + "PublicError": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "invalid_request_error", + "authentication_error", + "permission_error", + "conflict_error", + "rate_limit_error", + "api_error", + "upstream_error" + ] + }, + "code": { + "type": "string", + "enum": [ + "zero_knowledge_session", + "invalid_parameter", + "invalid_credential", + "entitlement_required", + "cloud_template_authority_restricted", + "gate_limit_reached", + "method_not_allowed", + "permission_denied", + "missing_scope", + "resource_not_found", + "hms_not_enabled", + "media_fetch_failed", + "resource_gone", + "feature_unavailable", + "stream_cursor_invalid", + "stream_cursor_expired", + "stream_connection_limit_reached", + "state_conflict", + "hybrid_routing_paused", + "hybrid_choice_required", + "hybrid_transition_ineligible", + "hybrid_automation_unavailable", + "hybrid_transport_unavailable", + "hybrid_target_reference_unavailable", + "send_outcome_unknown", + "idempotency_conflict", + "idempotency_in_progress", + "idempotency_completed", + "idempotency_outcome_unknown", + "call_claimed", + "call_not_ringing", + "connection_limit", + "unsupported_for_connection", + "call_permission_required", + "invalid_sip_trunk", + "calls_disabled", + "call_recipient_opted_out", + "call_destination_blocked", + "call_permission_request_limited", + "call_permission_granted", + "order_status_transition_invalid", + "order_cancellation_failed", + "call_opt_out_limit", + "number_restricted", + "sip_trunk_in_use", + "sip_trunk_revision_conflict", + "sip_trunk_limit", + "sip_unavailable", + "voice_not_enabled", + "provider_credential_invalid", + "provider_unavailable", + "asset_not_ready", + "voice_asset_in_use", + "voice_asset_revision_conflict", + "voice_unavailable", + "rate_limit_exceeded", + "internal_error", + "operation_not_supported", + "upstream_failure", + "service_unavailable", + "credential_verification_unavailable", + "session_not_ready", + "command_dispatch_failed", + "result_unknown", + "recipient_not_on_whatsapp", + "conversation_window_closed", + "template_not_approved", + "media_too_large", + "whatsapp_rate_limited", + "new_chat_limit_reached", + "whatsapp_account_restricted", + "whatsapp_groups_ineligible", + "whatsapp_group_creation_paused", + "whatsapp_group_limit_reached", + "whatsapp_group_full", + "whatsapp_group_suspended", + "whatsapp_group_has_no_participants", + "bansafe_suspended", + "bansafe_org_suspended", + "bansafe_cold_blocked", + "bansafe_cold_held", + "bansafe_throttled", + "bansafe_daily_allowance_reached", + "bansafe_accounting_unavailable", + "bansafe_send_outcome_unknown", + "campaigns_not_entitled", + "campaign_throughput_capped", + "campaign_approval_required", + "campaign_approval_stale", + "campaign_variables_missing" + ] + }, + "message": { + "type": "string" + }, + "param": { + "type": "string", + "nullable": true + }, + "request_id": { + "type": "string", + "description": "Matches the X-Request-Id response header. Include it when you contact support." + }, + "request_log_url": { + "type": "string", + "format": "uri", + "description": "Console page that shows this request. Returned only to organization and project credentials whose request was logged." + } + }, + "required": ["type", "code", "message", "param", "request_id"] + }, + "data": { + "type": "string", + "nullable": true, + "enum": [null], + "description": "Always null for a failed request." + }, + "docs": { + "type": "string", + "format": "uri" + } + }, + "required": ["error", "data", "docs"] + }, + "CampaignTestSendRequest": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "format": "uuid", + "description": "The number to send the test from. It must be connected, on a plan that includes Campaigns, and, when the campaign selects numbers, one of them." + }, + "to": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "description": "The seed recipient in international format. It must be the phone of a number connected to your team.", + "example": "+5511999990000" + }, + "sampleRecipientId": { + "type": "string", + "format": "uuid", + "description": "A recipient of this campaign whose variables fill the content. Without it and without `variables`, the campaign's first recipient, or its audience's first member, is the sample." + }, + "variables": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "maxLength": 1024 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "description": "Variable values for the test, at most 50. They override the sample recipient's values; without `sampleRecipientId`, only these values are used.", + "maxProperties": 50 + } + }, + "required": ["sessionId", "to"] + }, + "TestSendCampaignResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "$ref": "#/components/schemas/CampaignTestSend" + } + }, + "required": ["success", "data"] + }, + "CampaignTestSend": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "The test send's ID." + }, + "campaignId": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": ["sent"] + }, + "from": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "format": "uuid" + }, + "phone": { + "type": "string" + } + }, + "required": ["sessionId", "phone"], + "description": "The number the test was sent from." + }, + "to": { + "type": "string", + "description": "The seed recipient, in E.164 format." + }, + "messageId": { + "type": "string", + "nullable": true, + "description": "The WhatsApp message ID, when the send path returned one." + }, + "variables": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The variable values the content was rendered with." + }, + "createdAt": { + "type": "number", + "description": "When the test was admitted, in epoch milliseconds." + } + }, + "required": [ + "id", + "campaignId", + "status", + "from", + "to", + "messageId", + "variables", + "createdAt" + ] + }, + "CreateCampaignReplyFlowRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "flowKey": { + "type": "string", + "format": "uuid", + "description": "Existing flow key to add a revision to. Omit to start a new reply flow." + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000, + "description": "Revision number. Defaults to 1; each key and revision can be created once." + }, + "definition": { + "$ref": "#/components/schemas/CampaignReplyFlowDefinition" + } + }, + "required": ["name", "definition"], + "additionalProperties": false + }, + "CampaignReplyFlowDefinition": { + "type": "object", + "properties": { + "entryNodeId": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$" + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CampaignReplyFlowNode" + }, + "minItems": 2, + "maxItems": 12 + } + }, + "required": ["entryNodeId", "nodes"], + "additionalProperties": false, + "description": "An acyclic graph of 2 to 12 steps. The entry step waits for the first reply to the campaign message and sends nothing." + }, + "CampaignReplyFlowNode": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$" + }, + "send": { + "$ref": "#/components/schemas/CampaignReplyFlowSend" + }, + "edges": { + "type": "array", + "items": { + "type": "object", + "properties": { + "match": { + "$ref": "#/components/schemas/CampaignReplyFlowMatch" + }, + "to": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$" + } + }, + "required": ["match", "to"], + "additionalProperties": false + }, + "maxItems": 3 + } + }, + "required": ["id", "edges"], + "additionalProperties": false + }, + "CampaignReplyFlowSend": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["text"] + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": ["kind", "text"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["buttons"] + }, + "body": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "footer": { + "type": "string", + "maxLength": 60 + }, + "buttons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$", + "description": "Choice ID, 1 to 64 letters, digits, `.`, `_`, `:` or `-`, starting with a letter or digit." + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 20 + } + }, + "required": ["id", "text"], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 3 + } + }, + "required": ["kind", "body", "buttons"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["list"] + }, + "body": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "buttonText": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "footer": { + "type": "string", + "maxLength": 60 + }, + "rows": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$", + "description": "Choice ID, 1 to 64 letters, digits, `.`, `_`, `:` or `-`, starting with a letter or digit." + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 24 + }, + "description": { + "type": "string", + "maxLength": 72 + } + }, + "required": ["id", "title"], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 10 + } + }, + "required": ["kind", "body", "buttonText", "rows"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["flow_form"] + }, + "flowId": { + "type": "string", + "format": "uuid" + }, + "body": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "buttonText": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "footer": { + "type": "string", + "maxLength": 60 + }, + "screen": { + "type": "string", + "pattern": "^[A-Za-z0-9_]{1,64}$" + } + }, + "required": ["kind", "flowId", "body", "buttonText", "screen"], + "additionalProperties": false, + "description": "A published WhatsApp Flow form. Official numbers only." + } + ] + }, + "CampaignReplyFlowMatch": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["choice"] + }, + "replyKind": { + "type": "string", + "enum": ["button", "list"] + }, + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$", + "description": "Choice ID, 1 to 64 letters, digits, `.`, `_`, `:` or `-`, starting with a letter or digit." + } + }, + "required": ["kind", "replyKind", "id"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["keyword"] + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 64 + } + }, + "required": ["kind", "value"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["flow_submission"] + } + }, + "required": ["kind"], + "additionalProperties": false + } + ] + }, + "CreateCampaignReplyFlowResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "$ref": "#/components/schemas/CampaignReplyFlow" + } + }, + "required": ["success", "data"] + }, + "CampaignReplyFlow": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "flowKey": { + "type": "string", + "format": "uuid", + "description": "Groups the revisions of one reply flow." + }, + "revision": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "definition": { + "$ref": "#/components/schemas/CampaignReplyFlowDefinition" + }, + "createdAt": { + "type": "number" + } + }, + "required": [ + "id", + "flowKey", + "revision", + "name", + "definition", + "createdAt" + ] + }, + "GetCampaignReplyFlowResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "$ref": "#/components/schemas/CampaignReplyFlow" + } + }, + "required": ["success", "data"] + }, + "SetCampaignReplyFlowRequest": { + "type": "object", + "properties": { + "replyFlowId": { + "type": "string", + "nullable": true, + "format": "uuid", + "description": "Reply flow revision to run after replies, or null to detach it." + } + }, + "required": ["replyFlowId"], + "additionalProperties": false + }, + "SetCampaignReplyFlowResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "type": "object", + "properties": { + "campaignId": { + "type": "string", + "format": "uuid" + }, + "replyFlowId": { + "type": "string", + "nullable": true, + "format": "uuid" + } + }, + "required": ["campaignId", "replyFlowId"] + } + }, + "required": ["success", "data"] + } + }, + "headers": { + "X-RateLimit-Limit": { + "description": "Team request quota per rolling minute, when the per-team request limiter applies.", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the per-team quota; zero when this limiter rejects the request.", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix timestamp in seconds when the oldest counted request leaves the rolling window, when the per-team request limiter applies. Follow Retry-After on a rejected request.", + "schema": { + "type": "integer", + "minimum": 0 + } + } + } + } + }, + "platform": { + "sourcePath": "apps/api/docs/openapi.management.json", + "sourceSha256": "160fbc57b94ae0be9e5b1e9c183361582fe4b179e1b03c5917959d108990a057", + "paths": { + "/platform/projects/{projectId}/signal-custody": { + "get": { + "operationId": "getProjectSignalCustody", + "tags": ["signal-custody"], + "summary": "Read Project Signal custody default", + "description": "Returns the custody default for future Number reservations. Existing Number custody is immutable. Customer null defaults inherit the Project default.\n\nRequires `sessions:read`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "sessions:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/SignalCustodyDefault" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "put": { + "operationId": "updateProjectSignalCustody", + "tags": ["signal-custody"], + "summary": "Update Project Signal custody default", + "description": "Changes the default for future Number reservations with a revision check. Customer-owned custody requires beta admission and runtime availability. Existing Numbers keep their custody. Dashboard writes require a team owner or admin. Setting a Customer default to null restores Project inheritance.\n\nRequires `sessions:manage`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "sessions:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectSignalCustodyUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/SignalCustodyDefault" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/projects/{projectId}/customers/{customerId}/signal-custody": { + "get": { + "operationId": "getCustomerSignalCustody", + "tags": ["signal-custody"], + "summary": "Read Customer Signal custody default", + "description": "Returns the custody default for future Number reservations. Existing Number custody is immutable. Customer null defaults inherit the Project default.\n\nRequires `sessions:read`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "sessions:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "customerId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/SignalCustodyDefault" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "put": { + "operationId": "updateCustomerSignalCustody", + "tags": ["signal-custody"], + "summary": "Update Customer Signal custody default", + "description": "Changes the default for future Number reservations with a revision check. Customer-owned custody requires beta admission and runtime availability. Existing Numbers keep their custody. Dashboard writes require a team owner or admin. Setting a Customer default to null restores Project inheritance.\n\nRequires `sessions:manage`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "sessions:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "customerId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerSignalCustodyUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/SignalCustodyDefault" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/campaigns/{campaignId}/test-send": { + "post": { + "operationId": "testSendPlatformCampaign", + "tags": ["campaigns"], + "summary": "Send a test of a draft campaign", + "description": "Requires campaigns:manage and campaigns:read because the test can use stored recipient samples. Sends one copy of a draft campaign's content to a seed recipient, through the same rendering, sending and BanSafe checks as a launched campaign. The seed recipient must be a number connected to your team. The sending number must be connected, on a plan that includes Campaigns and, when the campaign selects numbers, one of them. A test is not a campaign recipient and is not counted in the campaign's statistics or webhooks. A team can send 20 tests an hour, and at most 5 an hour to one seed recipient. `429` means one of these limits was reached or BanSafe is pacing the sending number; follow `Retry-After`. `409 bansafe_cold_held` means the seed recipient has never written to the sending number and the campaign holds first messages. `409 state_conflict` means the campaign is not a draft or has A/B or weighted content alternatives, the sending number is a Hybrid Link number, or the seed recipient opted out. `504 result_unknown` means the outcome was not received in time and the test may still arrive. Send `projectId` in the body when using a team API key. `422 campaign_variables_missing` means the test's sample values, the mapped recipient value and the fallback all leave a variable in the campaign content empty.\n\nRequires `campaigns:manage` and `campaigns:read`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scopes": ["campaigns:manage", "campaigns:read"], + "x-required-scope-mode": "all", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "campaignId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "description": "Optional unique key, 1 to 255 bytes, that makes this request safe to retry for 24 hours. A retry with the same key, credential, `Polymorfa-Version` and request never runs it again: it returns `409 idempotency_completed` when the original succeeded, or the original server error. Reusing the key for a different request returns `409 idempotency_conflict`; a retry while the original is still running returns `409 idempotency_in_progress`, and a retry after an unrecorded outcome returns `409 idempotency_outcome_unknown`.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignTestSendRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/CampaignTestSend" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "402": { + "$ref": "#/components/responses/PaymentRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "422": { + "$ref": "#/components/responses/CampaignVariablesMissing" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "502": { + "$ref": "#/components/responses/UpstreamFailure" + }, + "503": { + "$ref": "#/components/responses/SendPathUnavailable" + }, + "504": { + "$ref": "#/components/responses/ResultUnknown" + } + } + } + }, + "/platform/campaign-reply-flows": { + "post": { + "operationId": "createPlatformCampaignReplyFlow", + "tags": ["campaigns"], + "summary": "Create campaign reply flow", + "description": "Creates an immutable reply flow revision: the steps a campaign runs when a recipient replies. Each step can send a text, reply buttons, a list, or a Flow form (Official numbers only), and branches on the button or list row chosen, an exact keyword, or a Flow form submission. Beta: returns `403` unless campaign reply flows are enabled for the team. `409` means this flow key and revision already exist.\n\nRequires `campaigns:manage`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePlatformCampaignReplyFlowRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/PlatformCampaignReplyFlow" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/campaign-reply-flows/{replyFlowId}": { + "get": { + "operationId": "getPlatformCampaignReplyFlow", + "tags": ["campaigns"], + "summary": "Get campaign reply flow", + "description": "Gets one reply flow revision in a project. Beta: returns `403` unless campaign reply flows are enabled for the team.\n\nRequires `campaigns:read`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "replyFlowId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/PlatformCampaignReplyFlow" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/platform/campaigns/{campaignId}/reply-flow": { + "put": { + "operationId": "setPlatformCampaignReplyFlow", + "tags": ["campaigns"], + "summary": "Set campaign reply flow", + "description": "Attaches a reply flow revision to a draft or scheduled campaign, or detaches it with `null`. The flow starts for each recipient once their message is sent. `409` means the campaign has already launched. Beta: returns `403` unless campaign reply flows are enabled for the team.\n\nRequires `campaigns:manage`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "campaignId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetPlatformCampaignReplyFlowRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": ["campaignId", "replyFlowId"], + "properties": { + "campaignId": { + "type": "string", + "format": "uuid" + }, + "replyFlowId": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/campaigns/{campaignId}/recipients/export": { + "get": { + "operationId": "exportCampaignRecipients", + "tags": ["campaigns"], + "summary": "Export campaign recipients", + "description": "Returns recipients as CSV in queue order. Unfiltered exports page up to 1,000 recipients per request, using the `Polymorfa-Next-Cursor` response header. Each page starts with a header row and reflects outcomes when requested. Filtered exports are one atomic page of up to 1,000 recipients; larger filtered results return 409, so changing statuses cannot silently omit matches between pages. The export contains outcomes and `failureReason` codes only, never message content or template variables.\n\nRequires `campaigns:read`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "campaignId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "query", + "required": false, + "description": "Project the campaign belongs to. Required with a team API key; a project token is bound to its own project and omits it.", + "schema": { + "type": "string", + "format": "uuid", + "description": "Project that owns the campaign. Required for team API keys; a project token is bound to its own project." + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "queued", + "sending", + "sent", + "delivered", + "read", + "failed", + "skipped" + ] + } + }, + { + "name": "reason", + "in": "query", + "required": false, + "description": "Only recipients whose `failureReason` is this code.", + "schema": { + "$ref": "#/components/schemas/CampaignRecipientFailureReason" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "The `Polymorfa-Next-Cursor` value from the previous unfiltered page. Filtered exports cannot be paged." + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 1000, + "description": "Recipients per unfiltered page, up to 1,000. Filtered exports require 1,000." + } + } + ], + "responses": { + "200": { + "description": "One CSV page.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + }, + "Polymorfa-Next-Cursor": { + "schema": { + "type": "string" + }, + "description": "Cursor for the next page. Absent on the last page." + } + }, + "content": { + "text/csv": { + "schema": { + "type": "string", + "description": "Header row, then one row per recipient. Columns: recipientId, phone, status, failureReason, attempts, variantKey, queuedAt, sentAt, deliveredAt, readAt, failedAt, respondedAt. Times are ISO 8601 in UTC. Empty fields are null. Text that begins with =, +, -, @, tab or carriage return is prefixed with an apostrophe, including phone numbers in international format. Machine readers should remove that leading apostrophe from phone cells." + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/campaigns/{campaignId}/links": { + "get": { + "operationId": "getCampaignLinks", + "tags": ["campaigns"], + "summary": "Get tracked link opens", + "description": "Beta: requires team enrollment in campaign link tracking. Returns opens of each tracked link in the campaign's link order. An open is a request to the short link from a person, or from an app or security scanner previewing the link; it is not proof of a click. `uniqueOpens` counts recipients who opened the link at least once; `totalOpens` counts every open. A team without the beta answers `403 feature_unavailable`.\n\nRequires `campaigns:read`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "campaignId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "query", + "required": false, + "description": "Project the campaign belongs to. Required with a team API key; a project token is bound to its own project and omits it.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CampaignTrackedLinkReport" + } + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/platform/campaigns/{campaignId}/approvals": { + "get": { + "operationId": "listCampaignLaunchApprovals", + "tags": ["campaigns"], + "summary": "List campaign launch approvals", + "description": "Lists a campaign's approval requests, newest first, at most 20. A request is created when a launch is held by the team's approval threshold, or when an AI agent asks through the hosted MCP server to launch, resume or move the start (`reasons` says which). `action` is the requested step. It records the frozen recipient count and the requested start time. `status` is `pending` until a team owner or admin approves or rejects it, `superseded` when a later launch request or a change to the campaign replaced it, and `expired` 72 hours after it was requested.\n\nRequires `campaigns:read`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "campaignId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "query", + "required": false, + "description": "Project the campaign belongs to. Required with a team API key; a project token is bound to its own project and omits it.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CampaignLaunchApproval" + } + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/platform/campaign-settings": { + "get": { + "operationId": "getCampaignSettings", + "tags": ["campaign-settings"], + "summary": "Get campaign settings", + "description": "Returns the team's campaign settings. `launchApproval.recipientThreshold` is the number of recipients above which a launch needs a team owner or admin; null means no launch needs approval. A team that never saved settings receives `launchApproval.recipientThreshold: null`.\n\nRequires a team API key with `campaigns:read`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/CampaignSettings" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/platform/audiences/from-campaign": { + "post": { + "operationId": "createAudienceFromCampaign", + "tags": ["audiences"], + "summary": "Create an audience from a campaign", + "description": "Creates an audience from the recipients of a previous campaign that had one `outcome`, to send them a follow-up campaign. The members are a snapshot taken when the request runs: later receipts, reads or replies on the source campaign do not change the audience. Numbers on the team's opt-out list are left out and counted in `optedOutCount`; each member keeps the template variables it had on the source campaign. Outcomes: `delivered` (the device confirmed delivery), `not_delivered` (sent, with no delivery confirmation), `read`, `not_read` (sent and not read, including sends never delivered), `replied`, `not_replied` (sent and no reply was attributed), and `failed` (confirmed failed sends; sends whose outcome is uncertain, such as a timeout waiting for WhatsApp, are excluded). Read outcomes depend on the recipient's read receipts. `404` means the campaign is not in your team (or not in `projectId` when given); `409` means no recipient that is not opted out has the outcome, and nothing is created.\n\nRequires a team API key with `campaigns:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "campaignId": { + "type": "string", + "format": "uuid", + "description": "The campaign whose recipients are selected." + }, + "projectId": { + "type": "string", + "format": "uuid", + "description": "Optional. When given, the campaign must belong to this project." + }, + "outcome": { + "type": "string", + "enum": [ + "delivered", + "not_delivered", + "read", + "not_read", + "replied", + "not_replied", + "failed" + ] + } + }, + "required": ["name", "campaignId", "outcome"] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/AudienceFromCampaignResult" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/audience-profiles": { + "get": { + "operationId": "listAudienceProfiles", + "tags": ["audience-profiles"], + "summary": "List audience profiles", + "description": "Lists customer-supplied profiles for the team with cursor pagination. Deleted profiles are omitted.\n\nRequires a team API key with `campaigns:read`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "phone": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{6,14}$" + }, + "revision": { + "type": "integer", + "minimum": 1 + }, + "tags": { + "type": "array", + "maxItems": 20, + "items": { + "type": "string", + "maxLength": 48 + } + }, + "attributes": { + "type": "object", + "maxProperties": 20, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "nullable": true + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "provenance": { + "type": "object", + "nullable": true, + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": ["api", "csv", "form"] + }, + "reference": { + "type": "string", + "maxLength": 128 + } + }, + "required": ["source", "reference"] + }, + "createdAt": { + "type": "integer", + "minimum": 0 + }, + "updatedAt": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "id", + "phone", + "revision", + "tags", + "attributes", + "provenance", + "createdAt", + "updatedAt" + ] + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": ["data", "page"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/platform/audience-profiles/{phone}": { + "get": { + "operationId": "getAudienceProfile", + "tags": ["audience-profiles"], + "summary": "Get an audience profile", + "description": "Gets one customer-supplied profile by its E.164 phone. Deleted profiles return 404.\n\nRequires a team API key with `campaigns:read`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "phone", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "phone": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{6,14}$" + }, + "revision": { + "type": "integer", + "minimum": 1 + }, + "tags": { + "type": "array", + "maxItems": 20, + "items": { + "type": "string", + "maxLength": 48 + } + }, + "attributes": { + "type": "object", + "maxProperties": 20, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "nullable": true + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "provenance": { + "type": "object", + "nullable": true, + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": ["api", "csv", "form"] + }, + "reference": { + "type": "string", + "maxLength": 128 + } + }, + "required": ["source", "reference"] + }, + "createdAt": { + "type": "integer", + "minimum": 0 + }, + "updatedAt": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "id", + "phone", + "revision", + "tags", + "attributes", + "provenance", + "createdAt", + "updatedAt" + ] + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "put": { + "operationId": "putAudienceProfile", + "tags": ["audience-profiles"], + "summary": "Create or update an audience profile", + "description": "Creates a bounded team profile, or replaces tags and attributes at the observed revision. A stale revision returns 409. To recreate a profile deleted in the last 30 days, send the `revision` the delete returned as `expectedRevision`; a put without it returns 409 with a message naming that revision. A consent assertion records the customer's statement; it does not clear an opt-out or authorize a send. This beta requires enrollment and release eligibility.\n\nRequires a team API key with `campaigns:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "phone", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "expectedRevision": { + "type": "integer", + "minimum": 1, + "description": "Required to edit or recreate a deleted profile; omit only for first creation." + }, + "tags": { + "type": "array", + "maxItems": 20, + "items": { + "type": "string", + "maxLength": 48 + } + }, + "attributes": { + "type": "object", + "maxProperties": 20, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "nullable": true + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": ["api", "csv", "form"] + }, + "reference": { + "type": "string", + "maxLength": 128 + } + }, + "required": ["source", "reference"] + }, + "consent": { + "type": "object", + "additionalProperties": false, + "properties": { + "purpose": { + "type": "string", + "enum": ["campaigns"] + }, + "source": { + "type": "string", + "enum": ["api", "csv", "form"] + }, + "reference": { + "type": "string", + "maxLength": 128 + }, + "allowed": { + "type": "boolean" + }, + "assertedAt": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + } + }, + "required": [ + "purpose", + "source", + "reference", + "allowed", + "assertedAt" + ] + } + }, + "required": ["tags", "attributes"] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "phone": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{6,14}$" + }, + "revision": { + "type": "integer", + "minimum": 1 + }, + "tags": { + "type": "array", + "maxItems": 20, + "items": { + "type": "string", + "maxLength": 48 + } + }, + "attributes": { + "type": "object", + "maxProperties": 20, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "nullable": true + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "provenance": { + "type": "object", + "nullable": true, + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": ["api", "csv", "form"] + }, + "reference": { + "type": "string", + "maxLength": 128 + } + }, + "required": ["source", "reference"] + }, + "createdAt": { + "type": "integer", + "minimum": 0 + }, + "updatedAt": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "id", + "phone", + "revision", + "tags", + "attributes", + "provenance", + "createdAt", + "updatedAt" + ] + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + }, + "delete": { + "operationId": "deleteAudienceProfile", + "tags": ["audience-profiles"], + "summary": "Delete an audience profile", + "description": "Removes profile fields, consent assertions and reusable audience-list memberships at the observed revision, and clears the preview row of each audience the phone belonged to. Recipients already added to campaigns (including drafts), previously uploaded files and opt-outs are not changed. The phone's deletion record is kept for 30 days; `revision` is its revision. During that time, adding the phone to an audience returns 409, and recreating the profile requires `expectedRevision` set to this revision. Deletion remains available after beta withdrawal.\n\nRequires a team API key with `campaigns:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "phone", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "expectedRevision", + "in": "query", + "required": true, + "description": "The profile revision you last read. A different current revision returns 409.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "deleted": { + "type": "boolean", + "enum": [true] + }, + "phone": { + "type": "string" + }, + "revision": { + "type": "integer", + "minimum": 2 + } + }, + "required": ["deleted", "phone", "revision"] + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/audience-profiles/{phone}/consent": { + "get": { + "operationId": "listAudienceProfileConsent", + "tags": ["audience-profiles"], + "summary": "List audience consent assertions", + "description": "Lists the customer's recorded campaign consent assertions for one profile. Assertions do not override opt-outs.\n\nRequires a team API key with `campaigns:read`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "phone", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "profileRevision": { + "type": "integer", + "minimum": 1 + }, + "purpose": { + "type": "string", + "enum": ["campaigns"] + }, + "source": { + "type": "string", + "enum": ["api", "csv", "form"] + }, + "reference": { + "type": "string" + }, + "allowed": { + "type": "boolean" + }, + "assertedAt": { + "type": "integer", + "minimum": 0 + }, + "createdAt": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "id", + "profileRevision", + "purpose", + "source", + "reference", + "allowed", + "assertedAt", + "createdAt" + ] + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": ["data", "page"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/platform/audience-profile-imports": { + "post": { + "operationId": "createAudienceProfileImport", + "tags": ["audience-profiles"], + "summary": "Start an audience profile CSV import", + "description": "Queues a bounded CSV import of customer-supplied team profiles. Exact CSV header names can be mapped to profile fields. At most one import is active per team. Files contain at most 10 MiB and 10,000 rows; the CSV is removed after completion or cancellation. The same replay key, file and mapping return the existing job. Requires beta enrollment.\n\nRequires a team API key with `campaigns:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "replayKey": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9._:-]+$" + }, + "csvBase64": { + "type": "string", + "minLength": 1, + "maxLength": 13981016, + "format": "byte", + "description": "UTF-8 CSV encoded as standard padded base64; decoded file at most 10 MiB and 10,000 data rows. The `tags` cell holds a JSON array of strings and the `attributes` cell a JSON object, quoted as CSV requires, for example `+15550100001,\"[\"\"vip\"\"]\",\"{\"\"city\"\":\"\"Lisbon\"\"}\"`." + }, + "mapping": { + "type": "object", + "additionalProperties": false, + "description": "Maps profile fields to exact CSV header names. Required when the CSV uses different headers; unmapped CSV columns are ignored.", + "properties": { + "phone": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "tags": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "attributes": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "expectedRevision": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "provenanceReference": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "consentAllowed": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "consentAssertedAt": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "consentReference": { + "type": "string", + "minLength": 1, + "maxLength": 64 + } + }, + "required": ["phone", "tags", "attributes"] + } + }, + "required": ["replayKey", "csvBase64"] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": ["queued", "completed", "cancelled"] + }, + "totalValid": { + "type": "integer", + "minimum": 0, + "description": "Rows that passed validation and will be written." + }, + "invalidCount": { + "type": "integer", + "minimum": 0, + "description": "Rows refused by validation; `invalidSamples` lists up to 20." + }, + "invalidSamples": { + "type": "array", + "maxItems": 20, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "row": { + "type": "integer", + "minimum": 2 + }, + "reason": { + "type": "string" + } + }, + "required": ["row", "reason"] + } + }, + "conflictSamples": { + "type": "array", + "maxItems": 20, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "row": { + "type": "integer", + "minimum": 2 + }, + "reason": { + "type": "string", + "enum": ["quota", "revision_conflict"] + } + }, + "required": ["row", "reason"] + } + }, + "processedValid": { + "type": "integer", + "minimum": 0, + "description": "Valid rows written so far." + }, + "createdCount": { + "type": "integer", + "minimum": 0, + "description": "Profiles created." + }, + "updatedCount": { + "type": "integer", + "minimum": 0, + "description": "Existing profiles updated." + }, + "conflictCount": { + "type": "integer", + "minimum": 0, + "description": "Valid rows not written because of the team's profile quota or a revision conflict; `conflictSamples` lists up to 20." + }, + "createdAt": { + "type": "integer", + "minimum": 0 + }, + "updatedAt": { + "type": "integer", + "minimum": 0 + }, + "completedAt": { + "type": "integer", + "minimum": 0, + "nullable": true + } + }, + "required": [ + "id", + "state", + "totalValid", + "invalidCount", + "invalidSamples", + "conflictSamples", + "processedValid", + "createdCount", + "updatedCount", + "conflictCount", + "createdAt", + "updatedAt", + "completedAt" + ] + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + } + } + } + }, + "/platform/audience-profile-imports/{jobId}": { + "get": { + "operationId": "getAudienceProfileImport", + "tags": ["audience-profiles"], + "summary": "Get an audience profile import report", + "description": "Returns aggregate counts and up to 20 row-number/reason samples. Raw phone numbers and custom values are not included. Reports expire 30 days after completion or cancellation.\n\nRequires a team API key with `campaigns:read`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "jobId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": ["queued", "completed", "cancelled"] + }, + "totalValid": { + "type": "integer", + "minimum": 0, + "description": "Rows that passed validation and will be written." + }, + "invalidCount": { + "type": "integer", + "minimum": 0, + "description": "Rows refused by validation; `invalidSamples` lists up to 20." + }, + "invalidSamples": { + "type": "array", + "maxItems": 20, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "row": { + "type": "integer", + "minimum": 2 + }, + "reason": { + "type": "string" + } + }, + "required": ["row", "reason"] + } + }, + "conflictSamples": { + "type": "array", + "maxItems": 20, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "row": { + "type": "integer", + "minimum": 2 + }, + "reason": { + "type": "string", + "enum": ["quota", "revision_conflict"] + } + }, + "required": ["row", "reason"] + } + }, + "processedValid": { + "type": "integer", + "minimum": 0, + "description": "Valid rows written so far." + }, + "createdCount": { + "type": "integer", + "minimum": 0, + "description": "Profiles created." + }, + "updatedCount": { + "type": "integer", + "minimum": 0, + "description": "Existing profiles updated." + }, + "conflictCount": { + "type": "integer", + "minimum": 0, + "description": "Valid rows not written because of the team's profile quota or a revision conflict; `conflictSamples` lists up to 20." + }, + "createdAt": { + "type": "integer", + "minimum": 0 + }, + "updatedAt": { + "type": "integer", + "minimum": 0 + }, + "completedAt": { + "type": "integer", + "minimum": 0, + "nullable": true + } + }, + "required": [ + "id", + "state", + "totalValid", + "invalidCount", + "invalidSamples", + "conflictSamples", + "processedValid", + "createdCount", + "updatedCount", + "conflictCount", + "createdAt", + "updatedAt", + "completedAt" + ] + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "delete": { + "operationId": "cancelAudienceProfileImport", + "tags": ["audience-profiles"], + "summary": "Cancel an audience profile import", + "description": "Stops the next batch and removes the CSV. Already committed profile edits remain. Cancellation and report reads remain available after beta withdrawal.\n\nRequires a team API key with `campaigns:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "jobId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": ["queued", "completed", "cancelled"] + }, + "totalValid": { + "type": "integer", + "minimum": 0, + "description": "Rows that passed validation and will be written." + }, + "invalidCount": { + "type": "integer", + "minimum": 0, + "description": "Rows refused by validation; `invalidSamples` lists up to 20." + }, + "invalidSamples": { + "type": "array", + "maxItems": 20, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "row": { + "type": "integer", + "minimum": 2 + }, + "reason": { + "type": "string" + } + }, + "required": ["row", "reason"] + } + }, + "conflictSamples": { + "type": "array", + "maxItems": 20, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "row": { + "type": "integer", + "minimum": 2 + }, + "reason": { + "type": "string", + "enum": ["quota", "revision_conflict"] + } + }, + "required": ["row", "reason"] + } + }, + "processedValid": { + "type": "integer", + "minimum": 0, + "description": "Valid rows written so far." + }, + "createdCount": { + "type": "integer", + "minimum": 0, + "description": "Profiles created." + }, + "updatedCount": { + "type": "integer", + "minimum": 0, + "description": "Existing profiles updated." + }, + "conflictCount": { + "type": "integer", + "minimum": 0, + "description": "Valid rows not written because of the team's profile quota or a revision conflict; `conflictSamples` lists up to 20." + }, + "createdAt": { + "type": "integer", + "minimum": 0 + }, + "updatedAt": { + "type": "integer", + "minimum": 0 + }, + "completedAt": { + "type": "integer", + "minimum": 0, + "nullable": true + } + }, + "required": [ + "id", + "state", + "totalValid", + "invalidCount", + "invalidSamples", + "conflictSamples", + "processedValid", + "createdCount", + "updatedCount", + "conflictCount", + "createdAt", + "updatedAt", + "completedAt" + ] + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/platform/optouts/confirmation-settings": { + "get": { + "operationId": "getStopConfirmationSettings", + "tags": ["optouts"], + "summary": "Get STOP confirmation settings", + "description": "Returns the team's optional acknowledgement preference. Confirmations are disabled by default. A locale is explicitly selected from English, Brazilian Portuguese or Spanish; the response includes the reviewed acknowledgement text. Reads remain available after beta withdrawal.\n\nRequires a team API key with `campaigns:read`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/StopConfirmationSettings" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + }, + "put": { + "operationId": "updateStopConfirmationSettings", + "tags": ["optouts"], + "summary": "Update STOP confirmation settings", + "description": "Replaces the team's preference using the observed revision. Enabling requires STOP confirmations beta access and a selected locale. A team owner or admin can also change it in the console. A new STOP opt-out queues at most one plain acknowledgement from the receiving connection; manual opt-outs, repeated STOP while suppressed and START produce none. A confirmation cannot undo suppression. Disabling remains available after beta withdrawal and cancels queued acknowledgements. A changed revision returns 409. Unknown send outcomes are never resent with a new operation identity.\n\nRequires a team API key with `campaigns:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateStopConfirmationSettingsInput" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/StopConfirmationSettings" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/optouts/confirmation-summary": { + "get": { + "operationId": "getStopConfirmationSummary", + "tags": ["optouts"], + "summary": "Get STOP confirmation outcome counts", + "description": "Returns aggregate acknowledgement outcomes from the last 30 days, without recipient phones or message content. Disabled is the preference state, queued is waiting, admitted may have been submitted, completed means the provider accepted the send rather than delivered it, failed is a proven skip or rejection, and unknown means the send outcome could not be established. All outcomes preserve the opt-out.\n\nRequires a team API key with `campaigns:read`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "campaigns:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/StopConfirmationSummary" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + } + }, + "components": { + "schemas": { + "SignalCustodyDefault": { + "type": "object", + "additionalProperties": false, + "properties": { + "signalModeDefault": { + "type": "string", + "enum": ["standard", "zero_knowledge", null], + "nullable": true + }, + "effectiveSignalMode": { + "type": "string", + "enum": ["standard", "zero_knowledge"] + }, + "revision": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["signalModeDefault", "effectiveSignalMode", "revision"] + }, + "ErrorEnvelope": { + "type": "object", + "required": ["error", "data", "docs"], + "properties": { + "error": { + "type": "object", + "required": ["type", "code", "message", "param", "request_id"], + "properties": { + "type": { + "type": "string", + "enum": [ + "invalid_request_error", + "authentication_error", + "permission_error", + "conflict_error", + "rate_limit_error", + "api_error", + "upstream_error" + ] + }, + "code": { + "type": "string", + "description": "Stable failure code. Use this value for programmatic handling." + }, + "message": { + "type": "string", + "description": "Safe explanation of the failure." + }, + "param": { + "type": "string", + "nullable": true, + "description": "Invalid request field, or null when no field applies." + }, + "request_id": { + "type": "string", + "description": "Matches the X-Request-Id response header. Include it when you contact support." + }, + "request_log_url": { + "type": "string", + "format": "uri", + "description": "Console page that shows this request. Returned only to organization and project credentials whose request was logged." + } + } + }, + "data": { + "type": "string", + "nullable": true, + "enum": [null], + "description": "Always null for a failed request." + }, + "docs": { + "type": "string", + "format": "uri", + "description": "Documentation link to this failure code." + } + } + }, + "ProjectSignalCustodyUpdate": { + "type": "object", + "additionalProperties": false, + "properties": { + "expectedRevision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "signalModeDefault": { + "type": "string", + "enum": ["standard", "zero_knowledge"] + } + }, + "required": ["expectedRevision", "signalModeDefault"] + }, + "CustomerSignalCustodyUpdate": { + "type": "object", + "additionalProperties": false, + "properties": { + "expectedRevision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "signalModeDefault": { + "type": "string", + "enum": ["standard", "zero_knowledge", null], + "nullable": true + } + }, + "required": ["expectedRevision", "signalModeDefault"] + }, + "CampaignTestSendRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "type": "string", + "format": "uuid", + "description": "Required when using a team API key. Project tokens derive the project from the credential." + }, + "sessionId": { + "type": "string", + "format": "uuid", + "description": "The number to send the test from. It must be connected, on a plan that includes Campaigns and, when the campaign selects numbers, one of them." + }, + "to": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "description": "The seed recipient in international format. It must be the phone of a number connected to your team.", + "example": "+5511999990000" + }, + "sampleRecipientId": { + "type": "string", + "format": "uuid", + "description": "A recipient of this campaign whose variables fill the content. Without it and without `variables`, the campaign's first recipient, or its audience's first member, is the sample." + }, + "variables": { + "type": "object", + "maxProperties": 50, + "additionalProperties": { + "oneOf": [ + { + "type": "string", + "maxLength": 1024 + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "description": "Variable values for the test. They override the sample recipient's values; without `sampleRecipientId`, only these values are used." + } + }, + "required": ["sessionId", "to"] + }, + "CampaignTestSend": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "The test send's ID." + }, + "campaignId": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": ["sent"] + }, + "from": { + "type": "object", + "additionalProperties": false, + "properties": { + "sessionId": { + "type": "string", + "format": "uuid" + }, + "phone": { + "type": "string" + } + }, + "required": ["sessionId", "phone"], + "description": "The number the test was sent from." + }, + "to": { + "type": "string", + "description": "The seed recipient, in E.164 format." + }, + "messageId": { + "type": "string", + "nullable": true, + "description": "The WhatsApp message ID, when the send path returned one." + }, + "variables": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The variable values the content was rendered with." + }, + "createdAt": { + "type": "number", + "description": "When the test was admitted, in Unix milliseconds." + } + }, + "required": [ + "id", + "campaignId", + "status", + "from", + "to", + "messageId", + "variables", + "createdAt" + ] + }, + "CreatePlatformCampaignReplyFlowRequest": { + "type": "object", + "additionalProperties": false, + "required": ["name", "definition"], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "flowKey": { + "type": "string", + "format": "uuid", + "description": "Existing flow key to add a revision to. Omit to start a new reply flow." + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000, + "description": "Defaults to 1; each key and revision can be created once." + }, + "definition": { + "$ref": "#/components/schemas/PlatformCampaignReplyFlowDefinition" + }, + "projectId": { + "type": "string", + "format": "uuid", + "description": "Required when using a team API key. Project tokens derive the project from the credential." + } + } + }, + "PlatformCampaignReplyFlowDefinition": { + "type": "object", + "additionalProperties": false, + "required": ["entryNodeId", "nodes"], + "description": "An acyclic graph of 2 to 12 steps. The entry step waits for the first reply and sends nothing. See the reply flows guide for step and branch shapes.", + "properties": { + "entryNodeId": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$" + }, + "nodes": { + "type": "array", + "minItems": 2, + "maxItems": 12, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "edges"], + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$" + }, + "send": { + "type": "object", + "required": ["kind"], + "additionalProperties": true, + "description": "`text` (`text`), `buttons` (`body`, up to 3 `buttons` with `id` and `text`), `list` (`body`, `buttonText`, up to 10 `rows`), or `flow_form` (`flowId`, `body`, `buttonText`, `screen`; Official numbers only).", + "properties": { + "kind": { + "type": "string", + "enum": ["text", "buttons", "list", "flow_form"] + } + } + }, + "edges": { + "type": "array", + "maxItems": 3, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["match", "to"], + "properties": { + "match": { + "type": "object", + "required": ["kind"], + "additionalProperties": true, + "description": "`choice` (`replyKind` `button` or `list`, `id`), `keyword` (`value`), or `flow_submission`.", + "properties": { + "kind": { + "type": "string", + "enum": ["choice", "keyword", "flow_submission"] + } + } + }, + "to": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "PlatformCampaignReplyFlow": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "flowKey", + "revision", + "name", + "definition", + "createdAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "flowKey": { + "type": "string", + "format": "uuid", + "description": "Groups the revisions of one reply flow." + }, + "revision": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "definition": { + "$ref": "#/components/schemas/PlatformCampaignReplyFlowDefinition" + }, + "createdAt": { + "type": "integer", + "description": "Unix milliseconds." + } + } + }, + "SetPlatformCampaignReplyFlowRequest": { + "type": "object", + "additionalProperties": false, + "required": ["replyFlowId"], + "properties": { + "replyFlowId": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Reply flow revision to run after replies, or null to detach it." + }, + "projectId": { + "type": "string", + "format": "uuid", + "description": "Required when using a team API key. Project tokens derive the project from the credential." + } + } + }, + "CampaignRecipientFailureReason": { + "type": "string", + "enum": [ + "opted_out", + "cold_held", + "campaign_cancelled", + "invalid_recipient", + "session_not_connected", + "ack_timeout", + "blocked_by_safety", + "send_failed", + "missing_variable", + "other" + ], + "description": "Why the recipient was not sent, or why its last attempt did not succeed. Null when nothing went wrong. `opted_out`: the number is on the team's opt-out list. `cold_held`: the number never wrote to the sending number and the campaign holds cold sends. `campaign_cancelled`: the campaign was stopped before this recipient was sent. `invalid_recipient`: the number is not a valid WhatsApp address. `session_not_connected`: the sending number was not connected. `ack_timeout`: WhatsApp did not acknowledge the send in time. `blocked_by_safety`: a safety check refused the send before it was attempted; the recipient waits for the campaign to resume. `send_failed`: WhatsApp refused the send, or its outcome could not be confirmed. `missing_variable`: the campaign was launched with `skipMissingVariables` and the recipient had no value and no fallback for a variable. `other`: any other reason. New codes may be added." + }, + "CampaignTrackedLinkReport": { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "type": "string", + "description": "Tracked link key.", + "example": "offer" + }, + "url": { + "type": "string", + "description": "Destination of the tracked link." + }, + "uniqueOpens": { + "type": "integer", + "minimum": 0, + "description": "Recipients who opened the link at least once." + }, + "totalOpens": { + "type": "integer", + "minimum": 0, + "description": "Every recorded open, including repeat opens." + } + }, + "required": ["key", "url", "uniqueOpens", "totalOpens"] + }, + "CampaignLaunchApproval": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "campaignId": { + "type": "string", + "format": "uuid" + }, + "action": { + "type": "string", + "enum": ["launch", "resume", "reschedule"], + "description": "The held command. Approving it runs that command." + }, + "status": { + "type": "string", + "enum": [ + "pending", + "approved", + "rejected", + "superseded", + "expired" + ] + }, + "reasons": { + "type": "array", + "items": { + "type": "string", + "enum": ["audience_threshold", "agent_request"] + }, + "description": "Why the command was held. `audience_threshold`: a launch would send to more recipients than the team's threshold. `agent_request`: an AI agent asked through the hosted MCP server." + }, + "recipientCount": { + "type": "integer", + "minimum": 0, + "description": "Recipients the command would send to: the frozen audience after opt-outs for a launch, the queued recipients for a resume or new start time." + }, + "scheduledAt": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "Requested start in Unix milliseconds; null starts when approved. Null for a resume." + }, + "requestedBy": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": ["type", "id"] + }, + "requestedAt": { + "type": "integer", + "format": "int64" + }, + "expiresAt": { + "type": "integer", + "format": "int64" + }, + "decidedBy": { + "type": "string", + "nullable": true, + "description": "The owner or admin who approved or rejected it." + }, + "decidedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "operationId": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "The launch operation an approval started." + } + }, + "required": [ + "id", + "campaignId", + "action", + "status", + "reasons", + "recipientCount", + "scheduledAt", + "requestedBy", + "requestedAt", + "expiresAt", + "decidedBy", + "decidedAt", + "operationId" + ] + }, + "CampaignSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "launchApproval": { + "type": "object", + "additionalProperties": false, + "properties": { + "recipientThreshold": { + "type": "integer", + "minimum": 1, + "maximum": 1000000, + "nullable": true, + "description": "Launches that would send to more recipients than this need a team owner or admin. Null turns approval off." + } + }, + "required": ["recipientThreshold"] + } + }, + "required": ["launchApproval"] + }, + "AudienceFromCampaignResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "source": { + "type": "string", + "enum": ["campaign"] + }, + "recipientCount": { + "type": "integer", + "minimum": 0, + "description": "Members in the new audience." + }, + "fileId": { + "type": "string", + "nullable": true + }, + "columns": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "sampleRow": { + "type": "object", + "nullable": true, + "additionalProperties": { + "type": "string" + } + }, + "mapping": { + "type": "object", + "nullable": true, + "additionalProperties": true + }, + "createdAt": { + "type": "integer" + }, + "updatedAt": { + "type": "integer" + }, + "sourceCampaignId": { + "type": "string", + "format": "uuid" + }, + "outcome": { + "type": "string", + "enum": [ + "delivered", + "not_delivered", + "read", + "not_read", + "replied", + "not_replied", + "failed" + ] + }, + "matchedCount": { + "type": "integer", + "minimum": 0, + "description": "Recipients of the source campaign with the outcome, before opt-outs were left out." + }, + "optedOutCount": { + "type": "integer", + "minimum": 0, + "description": "Matched recipients left out because the number is on the team's opt-out list." + } + }, + "required": [ + "id", + "name", + "source", + "recipientCount", + "fileId", + "columns", + "sampleRow", + "mapping", + "createdAt", + "updatedAt", + "sourceCampaignId", + "outcome", + "matchedCount", + "optedOutCount" + ] + }, + "CursorPageMetadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "nextCursor": { + "type": "string", + "nullable": true + }, + "hasMore": { + "type": "boolean" + } + }, + "required": ["nextCursor", "hasMore"] + }, + "StopConfirmationSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "teamId": { + "type": "string", + "format": "uuid" + }, + "enabled": { + "type": "boolean" + }, + "locale": { + "type": "string", + "enum": ["en", "pt-BR", "es", null], + "nullable": true + }, + "revision": { + "type": "integer", + "minimum": 0 + }, + "textRevision": { + "type": "integer", + "enum": [1] + }, + "acknowledgement": { + "type": "string", + "nullable": true + }, + "updatedAt": { + "type": "integer", + "format": "int64", + "nullable": true + } + }, + "required": [ + "teamId", + "enabled", + "locale", + "revision", + "textRevision", + "acknowledgement", + "updatedAt" + ] + }, + "UpdateStopConfirmationSettingsInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "teamId": { + "type": "string", + "format": "uuid", + "description": "The teamId returned by the settings read. Must match the authenticated team." + }, + "enabled": { + "type": "boolean" + }, + "locale": { + "type": "string", + "enum": ["en", "pt-BR", "es", null], + "nullable": true, + "description": "Required when enabled. Never inferred from a phone number." + }, + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 2147483646 + } + }, + "required": ["teamId", "enabled", "locale", "revision"] + }, + "StopConfirmationSummary": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "counts": { + "type": "object", + "additionalProperties": false, + "properties": { + "queued": { + "type": "integer", + "minimum": 0 + }, + "admitted": { + "type": "integer", + "minimum": 0 + }, + "completed": { + "type": "integer", + "minimum": 0 + }, + "failed": { + "type": "integer", + "minimum": 0 + }, + "unknown": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "queued", + "admitted", + "completed", + "failed", + "unknown" + ] + } + }, + "required": ["enabled", "counts"] + } + }, + "responses": { + "BadRequest": { + "description": "A path, query, or request-body value did not pass validation.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "Unauthorized": { + "description": "Missing/invalid credential for this audience.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "Forbidden": { + "description": "Authenticated but lacks the required scope/role/audience.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "NotFound": { + "description": "The requested resource was not found inside the authenticated team or project scope.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "Conflict": { + "description": "The request conflicts with the resource's current state.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "PaymentRequired": { + "description": "The number's plan does not include this capability.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "CampaignVariablesMissing": { + "description": "`campaign_variables_missing`: a recipient (or the test's sample values) has no value and no fallback for a variable in the campaign content. Nothing was sent.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "RateLimited": { + "description": "A limit was reached. Wait for `Retry-After` before retrying.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + }, + "Retry-After": { + "schema": { + "type": "integer", + "minimum": 1 + }, + "description": "Seconds to wait before retrying." + }, + "Polymorfa-RateLimit-Reason": { + "schema": { + "type": "string" + }, + "description": "The limit that was reached. See the rate-limit reasons in the error reference." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "UpstreamFailure": { + "description": "`upstream_failure`: the messaging provider could not complete the send.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "SendPathUnavailable": { + "description": "The send path is unavailable or the sending number is not ready (`session_not_ready`, `command_dispatch_failed`, `service_unavailable`, or `bansafe_accounting_unavailable`). No message was sent.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "ResultUnknown": { + "description": "`result_unknown`: the outcome was not received before the deadline. The message may still arrive; check before repeating the request.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "PayloadTooLarge": { + "description": "Request body exceeds the endpoint limit.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + } + } +} diff --git a/contracts/campaign-frequency-cap.json b/contracts/campaign-frequency-cap.json index be5be2aa..a082465f 100644 --- a/contracts/campaign-frequency-cap.json +++ b/contracts/campaign-frequency-cap.json @@ -1,11 +1,11 @@ { "repository": "polymorfa/polymorfa", - "commit": "e404a727ddb4abf42a8df768a5336fad922cc99a", + "commit": "9bb1e2ed92d8b4a07ff727f8e7ae185498f0868f", "note": "Contact frequency cap beta, merged backend #601 and Console #609. Full snapshots retain their independent pins.", "sources": { "messaging": { "sourcePath": "apps/api/docs/openapi.json", - "sourceSha256": "9d8faf13be970bee800ad15e308c130bc4ae8703865ae99278b9ae315cc27638", + "sourceSha256": "6d5b5477a86a85504f921933bf7b26f2c47506e21380fe73c8a5ff1002163b28", "schemas": {}, "fields": { "VoiceAssetFailedPayload.failureReason": { @@ -634,7 +634,7 @@ }, "platform": { "sourcePath": "apps/api/docs/openapi.management.json", - "sourceSha256": "e28256daac1e8efd84998d7399b74c3200623c19ac807a4b0730523a667976ff", + "sourceSha256": "bf4c6056b75e2fb0293b48edc5d8c12c36cdcbd194b9d11159f59522d30badc3", "schemas": { "CampaignRecipientFailureReason": { "type": "string", diff --git a/contracts/campaign-segments-drip.json b/contracts/campaign-segments-drip.json index 314a4eb4..bbd7e8e9 100644 --- a/contracts/campaign-segments-drip.json +++ b/contracts/campaign-segments-drip.json @@ -1,10 +1,10 @@ { "repository": "polymorfa/polymorfa", - "commit": "f46ebd5162453e812dafa76ce02b420ddabe6921", + "commit": "9bb1e2ed92d8b4a07ff727f8e7ae185498f0868f", "sources": { "messaging": { "sourcePath": "apps/api/docs/openapi.json", - "sourceSha256": "a1e4538d57a47169539d4a3254585df473d23da46fd67e89dfbf8aacaeff8a6b", + "sourceSha256": "6d5b5477a86a85504f921933bf7b26f2c47506e21380fe73c8a5ff1002163b28", "schemas": { "CampaignDrip": { "type": "object", @@ -948,7 +948,7 @@ }, "platform": { "sourcePath": "apps/api/docs/openapi.management.json", - "sourceSha256": "ccb468d7e0540f5f1b0b1e881b8965c54cf51cfbc85ad03606a690af0d3b6096", + "sourceSha256": "bf4c6056b75e2fb0293b48edc5d8c12c36cdcbd194b9d11159f59522d30badc3", "schemas": { "AudienceSegment": { "type": "object", diff --git a/contracts/coverage.json b/contracts/coverage.json index 092a92ed..2305b40e 100644 --- a/contracts/coverage.json +++ b/contracts/coverage.json @@ -1,5 +1,5 @@ { - "schemaVersion": 1, + "schemaVersion": 2, "sourceCommit": "171a9683ff45d11a4b5d3917dc428ad2722b0d99", "excludedContracts": [ { @@ -17,6 +17,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.deleteCollection" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.DeleteCollection" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.delete_collection" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->deleteCollection" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.DeleteCollectionAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.delete_collection" } }, { @@ -28,6 +48,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.deleteProduct" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.DeleteProduct" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.delete_product" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->deleteProduct" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.DeleteProductAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.delete_product" } }, { @@ -39,6 +79,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.deleteCoverPhoto" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.DeleteCoverPhoto" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.delete_cover_photo" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->deleteCoverPhoto" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.DeleteCoverPhotoAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.delete_cover_photo" } }, { @@ -50,6 +110,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.quickReplies.delete" + }, + "go": { + "status": "covered", + "method": "MessagingClient.QuickReplies.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.quick_replies.delete" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$quickReplies->delete" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.QuickReplies.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.quick_replies.delete" } }, { @@ -61,6 +141,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.delete" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.delete" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->delete" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.delete" } }, { @@ -72,6 +172,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.deleteMessage" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.DeleteMessage" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.delete_message" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->deleteMessage" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.DeleteMessageAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.delete_message" } }, { @@ -83,6 +203,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.delete" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.delete" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.delete" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.delete" } }, { @@ -94,6 +234,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.labels.delete" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Labels.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.labels.delete" + }, + "php": { + "status": "covered", + "method": "MessagingClient.labels.delete" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Labels.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.labels.delete" } }, { @@ -105,6 +265,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.delete" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.delete" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.delete" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.delete" } }, { @@ -116,6 +296,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.profile.deletePicture" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Profile.DeletePicture" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.profile.delete_picture" + }, + "php": { + "status": "covered", + "method": "MessagingClient.profile.deletePicture" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Profile.DeletePictureAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.profile.delete_picture" } }, { @@ -127,6 +327,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.cloudTemplates.delete" + }, + "go": { + "status": "covered", + "method": "MessagingClient.CloudTemplates.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.cloud_templates.delete" + }, + "php": { + "status": "covered", + "method": "MessagingClient::cloudTemplates.delete" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.CloudTemplates.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.cloud_templates.delete" } }, { @@ -138,6 +358,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.templates.delete" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Templates.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.templates.delete" + }, + "php": { + "status": "covered", + "method": "MessagingClient::templates.delete" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Templates.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.templates.delete" } }, { @@ -149,6 +389,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.quickLinks.cancel" + }, + "go": { + "status": "covered", + "method": "MessagingClient.QuickLinks.Cancel" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.quick_links.cancel" + }, + "php": { + "status": "covered", + "method": "MessagingClient.quickLinks.cancel" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.QuickLinks.CancelAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.quick_links.cancel" } }, { @@ -160,6 +420,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.end" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.End" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.end" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->end" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.EndAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.end" } }, { @@ -171,6 +451,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.webhooks.delete" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Webhooks.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.webhooks.delete" + }, + "php": { + "status": "covered", + "method": "MessagingClient.webhooks.delete" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Webhooks.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.webhooks.delete" } }, { @@ -182,6 +482,26 @@ "typescript": { "status": "covered", "method": "SystemClient.health" + }, + "go": { + "status": "covered", + "method": "SystemClient.Health" + }, + "python": { + "status": "covered", + "method": "AsyncSystemClient.health" + }, + "php": { + "status": "covered", + "method": "SystemClient::health" + }, + "dotnet": { + "status": "covered", + "method": "SystemClient.HealthAsync" + }, + "rust": { + "status": "covered", + "method": "SystemClient.health" } }, { @@ -193,6 +513,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.getCatalog" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.GetCatalog" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.get_catalog" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->getCatalog" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.GetCatalogAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.get_catalog" } }, { @@ -204,6 +544,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.listCollections" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.ListCollections" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.list_collections" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->listCollections" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.ListCollectionsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.list_collections" } }, { @@ -215,6 +575,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.getCollection" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.GetCollection" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.get_collection" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->getCollection" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.GetCollectionAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.get_collection" } }, { @@ -226,6 +606,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.getMerchantCompliance" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.GetMerchantCompliance" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.get_merchant_compliance" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->getMerchantCompliance" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.GetMerchantComplianceAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.get_merchant_compliance" } }, { @@ -237,6 +637,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.getEligibility" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.GetEligibility" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.get_eligibility" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->getEligibility" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.GetEligibilityAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.get_eligibility" } }, { @@ -248,6 +668,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.getLinkedAccounts" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.GetLinkedAccounts" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.get_linked_accounts" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->getLinkedAccounts" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.GetLinkedAccountsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.get_linked_accounts" } }, { @@ -259,6 +699,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.getProduct" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.GetProduct" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.get_product" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->getProduct" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.GetProductAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.get_product" } }, { @@ -270,6 +730,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.getProfile" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.GetProfile" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.get_profile" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->getProfile" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.GetProfileAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.get_profile" } }, { @@ -281,10 +761,30 @@ "typescript": { "status": "covered", "method": "MessagingClient.quickReplies.list" - } - }, - { - "family": "messaging", + }, + "go": { + "status": "covered", + "method": "MessagingClient.QuickReplies.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.quick_replies.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$quickReplies->list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.QuickReplies.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.quick_replies.list" + } + }, + { + "family": "messaging", "method": "GET", "path": "/messaging/{session}/call-permissions/{to}", "operationId": "getCallPermission", @@ -292,6 +792,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.retrieveCallPermission" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.RetrieveCallPermission" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.retrieve_call_permission" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->retrieveCallPermission" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.RetrieveCallPermissionAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.retrieve_call_permission" } }, { @@ -303,6 +823,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.list" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.list" } }, { @@ -314,6 +854,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.retrieve" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.retrieve" } }, { @@ -325,6 +885,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.listMessageUpdates" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.ListMessageUpdates" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.list_message_updates" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->listMessageUpdates" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.ListMessageUpdatesAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.list_message_updates" } }, { @@ -336,6 +916,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.listMessages" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.ListMessages" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.list_messages" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->listMessages" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.ListMessagesAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.list_messages" } }, { @@ -347,6 +947,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.list" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.list" } }, { @@ -358,6 +978,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.retrieve" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.retrieve" } }, { @@ -369,6 +1009,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.listMessages" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.ListMessages" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.list_messages" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->listMessages" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.ListMessagesAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.list_messages" } }, { @@ -380,6 +1040,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.retrieveMessage" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.RetrieveMessage" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.retrieve_message" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->retrieveMessage" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.RetrieveMessageAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.retrieve_message" } }, { @@ -391,6 +1071,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.downloadMessageMedia" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.DownloadMessageMedia" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.download_message_media" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->downloadMessageMedia" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.DownloadMessageMediaAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.download_message_media_stream" } }, { @@ -402,6 +1102,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.getServiceWindow" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.GetServiceWindow" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.get_service_window" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->getServiceWindow" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.GetServiceWindowAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.get_service_window" } }, { @@ -413,6 +1133,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.sessions.getCloudCredentialHealth" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Sessions.GetCloudCredentialHealth" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.sessions.get_cloud_credential_health" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$sessions->getCloudCredentialHealth" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Sessions.GetCloudCredentialHealthAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.get_cloud_credential_health" } }, { @@ -424,6 +1164,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.contacts.list" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Contacts.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.contacts.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient.contacts.list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Contacts.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.contacts.list" } }, { @@ -435,6 +1195,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.contacts.retrieve" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Contacts.Check" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.contacts.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient.contacts.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Contacts.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.contacts.retrieve" } }, { @@ -446,6 +1226,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.contacts.businessProfile" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Contacts.BusinessProfile" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.contacts.business_profile" + }, + "php": { + "status": "covered", + "method": "MessagingClient.contacts.businessProfile" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Contacts.BusinessProfileAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.contacts.business_profile" } }, { @@ -457,6 +1257,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.contacts.devices" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Contacts.Devices" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.contacts.devices" + }, + "php": { + "status": "covered", + "method": "MessagingClient.contacts.devices" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Contacts.DevicesAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.contacts.devices" } }, { @@ -468,6 +1288,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.contacts.info" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Contacts.Info" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.contacts.info" + }, + "php": { + "status": "covered", + "method": "MessagingClient.contacts.info" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Contacts.InfoAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.contacts.info" } }, { @@ -479,6 +1319,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.contacts.picture" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Contacts.Picture" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.contacts.picture" + }, + "php": { + "status": "covered", + "method": "MessagingClient.contacts.picture" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Contacts.PictureAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.contacts.picture" } }, { @@ -490,6 +1350,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.contacts.blocklist" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Contacts.Blocklist" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.contacts.blocklist" + }, + "php": { + "status": "covered", + "method": "MessagingClient.contacts.blocklist" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Contacts.BlocklistAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.contacts.blocklist" } }, { @@ -501,6 +1381,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.contacts.check" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Contacts.Check" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.contacts.check" + }, + "php": { + "status": "covered", + "method": "MessagingClient.contacts.check" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Contacts.CheckAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.contacts.check" } }, { @@ -512,6 +1412,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.list" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.list" } }, { @@ -523,17 +1443,57 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.retrieve" - } - }, - { - "family": "messaging", - "method": "GET", - "path": "/messaging/{session}/groups/{id}/invite-code", - "operationId": "getGroupInviteCode", - "fingerprint": "612a3c63b52090bab7ee5abd3f760daa8f30cb505096b1dcf59721f83875ccbe", - "typescript": { + }, + "go": { "status": "covered", - "method": "MessagingClient.groups.getInviteCode" + "method": "MessagingClient.Groups.GetJoinInfo" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.retrieve" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/groups/{id}/invite-code", + "operationId": "getGroupInviteCode", + "fingerprint": "612a3c63b52090bab7ee5abd3f760daa8f30cb505096b1dcf59721f83875ccbe", + "typescript": { + "status": "covered", + "method": "MessagingClient.groups.getInviteCode" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.GetInviteCode" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.get_invite_code" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.getInviteCode" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.GetInviteCodeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.get_invite_code" } }, { @@ -545,6 +1505,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.listParticipants" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.ListParticipants" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.list_participants" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.listParticipants" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.ListParticipantsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.list_participants" } }, { @@ -556,6 +1536,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.getJoinInfo" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.GetJoinInfo" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.get_join_info" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.getJoinInfo" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.GetJoinInfoAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.get_join_info" } }, { @@ -567,6 +1567,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.hybridLink.state" + }, + "go": { + "status": "covered", + "method": "MessagingClient.HybridLink.State" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.hybrid_link.state" + }, + "php": { + "status": "covered", + "method": "MessagingClient::hybridLink.state" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.HybridLink.StateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.hybrid_link.state" } }, { @@ -578,6 +1598,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.identities.resolve" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Identities.Resolve" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.identities.resolve" + }, + "php": { + "status": "covered", + "method": "MessagingClient.identities.resolve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Identities.ResolveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.identities.resolve" } }, { @@ -589,6 +1629,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.labels.list" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Labels.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.labels.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient.labels.list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Labels.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.labels.list" } }, { @@ -600,6 +1660,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.labels.listForChat" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Labels.ListForChat" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.labels.list_for_chat" + }, + "php": { + "status": "covered", + "method": "MessagingClient.labels.listForChat" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Labels.ListForChatAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.labels.list_for_chat" } }, { @@ -611,6 +1691,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.sessions.getMetaPricing" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Sessions.GetMetaPricing" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.sessions.get_meta_pricing" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$sessions->getMetaPricing" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Sessions.GetMetaPricingAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.get_meta_pricing" } }, { @@ -622,6 +1722,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.observationPolicies.retrieveForSession" + }, + "go": { + "status": "covered", + "method": "MessagingClient.ObservationPolicies.RetrieveForSession" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.observation_policies.retrieve_for_session" + }, + "php": { + "status": "covered", + "method": "MessagingClient::observationPolicies.retrieveForSession" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.ObservationPolicies.RetrieveForSessionAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.observation_policies.retrieve_for_session" } }, { @@ -633,6 +1753,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.list" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.list" } }, { @@ -644,6 +1784,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.retrieve" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.retrieve" } }, { @@ -655,6 +1815,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.getInviteLink" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.GetInviteLink" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.get_invite_link" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.getInviteLink" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.GetInviteLinkAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.get_invite_link" } }, { @@ -666,6 +1846,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.listJoinRequests" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.ListJoinRequests" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.list_join_requests" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.listJoinRequests" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.ListJoinRequestsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.list_join_requests" } }, { @@ -677,6 +1877,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.messages.operationStatus" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Messages.OperationStatus" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.messages.operation_status" + }, + "php": { + "status": "covered", + "method": "MessagingClient::messages.operationStatus" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Messages.OperationStatusAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.messages.operation_status" } }, { @@ -688,6 +1908,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.sessions.qr" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Sessions.QR" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.sessions.qr" + }, + "php": { + "status": "covered", + "method": "MessagingClient.sessions.qr" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Sessions.QrAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.qr" } }, { @@ -699,6 +1939,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.presence.get" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Presence.Get" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.presence.get" + }, + "php": { + "status": "covered", + "method": "MessagingClient.presence.get" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Presence.GetAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.presence.get" } }, { @@ -710,6 +1970,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.presence.getForChat" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Presence.GetForChat" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.presence.get_for_chat" + }, + "php": { + "status": "covered", + "method": "MessagingClient.presence.getForChat" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Presence.GetForChatAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.presence.get_for_chat" } }, { @@ -721,6 +2001,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.privacy.get" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Privacy.Get" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.privacy.get" + }, + "php": { + "status": "covered", + "method": "MessagingClient.privacy.get" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Privacy.GetAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.privacy.get" } }, { @@ -732,6 +2032,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.profile.get" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Profile.Get" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.profile.get" + }, + "php": { + "status": "covered", + "method": "MessagingClient.profile.get" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Profile.GetAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.profile.get" } }, { @@ -743,6 +2063,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.banSafe.getSessionSafeMode" + }, + "go": { + "status": "covered", + "method": "MessagingClient.BanSafe.GetSessionSafeMode" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.ban_safe.get_session_safe_mode" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$banSafe->getSessionSafeMode" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.BanSafe.GetSessionSafeModeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.ban_safe.get_session_safe_mode" } }, { @@ -754,6 +2094,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.cloudTemplates.list" + }, + "go": { + "status": "covered", + "method": "MessagingClient.CloudTemplates.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.cloud_templates.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient::cloudTemplates.list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.CloudTemplates.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.cloud_templates.list" } }, { @@ -765,17 +2125,57 @@ "typescript": { "status": "covered", "method": "MessagingClient.cloudTemplates.retrieve" - } - }, - { - "family": "messaging", - "method": "GET", - "path": "/messaging/{session}/users/{id}/security-code", - "operationId": "getUserSecurityCode", - "fingerprint": "69f03a5c99714e7e5c6adc4d77c656596ccccedc1a937e5b603751167ca058c1", + }, + "go": { + "status": "covered", + "method": "MessagingClient.CloudTemplates.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.cloud_templates.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient::cloudTemplates.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.CloudTemplates.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.cloud_templates.retrieve" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/users/{id}/security-code", + "operationId": "getUserSecurityCode", + "fingerprint": "69f03a5c99714e7e5c6adc4d77c656596ccccedc1a937e5b603751167ca058c1", "typescript": { "status": "covered", "method": "MessagingClient.users.getSecurityCode" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Users.GetSecurityCode" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.users.get_security_code" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$users->getSecurityCode" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Users.GetSecurityCodeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.users.get_security_code" } }, { @@ -787,6 +2187,26 @@ "typescript": { "status": "covered", "method": "BridgeClient.routes.resolve" + }, + "go": { + "status": "covered", + "method": "BridgeClient.Resolve" + }, + "python": { + "status": "covered", + "method": "AsyncBridgeClient.routes.resolve" + }, + "php": { + "status": "covered", + "method": "BridgeClient::routes.resolve" + }, + "dotnet": { + "status": "covered", + "method": "BridgeClient.Routes.ResolveAsync" + }, + "rust": { + "status": "covered", + "method": "BridgeClient.routes.resolve" } }, { @@ -798,6 +2218,26 @@ "typescript": { "status": "covered", "method": "SystemClient.status" + }, + "go": { + "status": "covered", + "method": "SystemClient.Status" + }, + "python": { + "status": "covered", + "method": "AsyncSystemClient.status" + }, + "php": { + "status": "covered", + "method": "SystemClient::status" + }, + "dotnet": { + "status": "covered", + "method": "SystemClient.StatusAsync" + }, + "rust": { + "status": "covered", + "method": "SystemClient.status" } }, { @@ -809,6 +2249,26 @@ "typescript": { "status": "covered", "method": "SystemClient.version" + }, + "go": { + "status": "covered", + "method": "SystemClient.Version" + }, + "python": { + "status": "covered", + "method": "AsyncSystemClient.version" + }, + "php": { + "status": "covered", + "method": "SystemClient::version" + }, + "dotnet": { + "status": "covered", + "method": "SystemClient.VersionAsync" + }, + "rust": { + "status": "covered", + "method": "SystemClient.version" } }, { @@ -820,6 +2280,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.media.download" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Media.Download" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.media.download" + }, + "php": { + "status": "covered", + "method": "MessagingClient::media.download" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Media.DownloadAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.media.download_stream" } }, { @@ -831,6 +2311,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.media.retrieve" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Media.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.media.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient::media.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Media.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.media.retrieve" } }, { @@ -842,6 +2342,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.banSafe.getProjectHealthPolicy" + }, + "go": { + "status": "covered", + "method": "MessagingClient.BanSafe.GetProjectHealthPolicy" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.ban_safe.get_project_health_policy" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$banSafe->getProjectHealthPolicy" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.BanSafe.GetProjectHealthPolicyAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.ban_safe.get_project_health_policy" } }, { @@ -853,6 +2373,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.banSafe.getProjectInsuranceEvidence" + }, + "go": { + "status": "covered", + "method": "MessagingClient.BanSafe.GetProjectInsuranceEvidence" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.ban_safe.get_project_insurance_evidence" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$banSafe->getProjectInsuranceEvidence" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.BanSafe.GetProjectInsuranceEvidenceAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.ban_safe.get_project_insurance_evidence" } }, { @@ -864,6 +2404,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.observationPolicies.retrieveForProject" + }, + "go": { + "status": "covered", + "method": "MessagingClient.ObservationPolicies.RetrieveForProject" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.observation_policies.retrieve_for_project" + }, + "php": { + "status": "covered", + "method": "MessagingClient::observationPolicies.retrieveForProject" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.ObservationPolicies.RetrieveForProjectAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.observation_policies.retrieve_for_project" } }, { @@ -875,6 +2435,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.banSafe.getProjectSafeMode" + }, + "go": { + "status": "covered", + "method": "MessagingClient.BanSafe.GetProjectSafeMode" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.ban_safe.get_project_safe_mode" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$banSafe->getProjectSafeMode" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.BanSafe.GetProjectSafeModeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.ban_safe.get_project_safe_mode" } }, { @@ -886,6 +2466,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.banSafe.getProjectWarmupPlan" + }, + "go": { + "status": "covered", + "method": "MessagingClient.BanSafe.GetProjectWarmupPlan" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.ban_safe.get_project_warmup_plan" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$banSafe->getProjectWarmupPlan" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.BanSafe.GetProjectWarmupPlanAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.ban_safe.get_project_warmup_plan" } }, { @@ -897,6 +2497,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.list" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.list" } }, { @@ -908,6 +2528,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.retrieve" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.retrieve" } }, { @@ -919,6 +2559,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.analytics" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.Analytics" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.analytics" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.analytics" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.AnalyticsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.analytics" } }, { @@ -930,6 +2590,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.listRecipients" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.ListRecipients" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.recipients" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.listRecipients" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.ListRecipientsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.list_recipients" } }, { @@ -941,6 +2621,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.templates.list" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Templates.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.templates.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient::templates.list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Templates.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.templates.list" } }, { @@ -952,6 +2652,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.templates.retrieve" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Templates.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.templates.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient::templates.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Templates.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.templates.retrieve" } }, { @@ -963,6 +2683,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.quickLinks.retrieve" + }, + "go": { + "status": "covered", + "method": "MessagingClient.QuickLinks.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.quick_links.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient.quickLinks.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.QuickLinks.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.quick_links.retrieve" } }, { @@ -974,6 +2714,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.quickLinks.availability" + }, + "go": { + "status": "covered", + "method": "MessagingClient.QuickLinks.Availability" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.quick_links.availability" + }, + "php": { + "status": "covered", + "method": "MessagingClient::quickLinks.availability" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.QuickLinks.AvailabilityAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.quick_links.availability" } }, { @@ -985,6 +2745,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.hybridLink.getPolicy" + }, + "go": { + "status": "covered", + "method": "MessagingClient.HybridLink.GetPolicy" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.hybrid_link.get_policy" + }, + "php": { + "status": "covered", + "method": "MessagingClient::hybridLink.getPolicy" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.HybridLink.GetPolicyAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.hybrid_link.get_policy" } }, { @@ -996,6 +2776,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.testing.listEventFixtures" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Testing.ListEventFixtures" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.testing.list_event_fixtures" + }, + "php": { + "status": "covered", + "method": "MessagingClient::testing.listEventFixtures" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Testing.ListEventFixturesAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.testing.list_event_fixtures" } }, { @@ -1007,6 +2807,29 @@ "typescript": { "status": "excluded", "reason": "used internally by the Calls SDK; not part of the public API" + }, + "go": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + "python": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + "rust": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "not-server-sdk" } }, { @@ -1018,17 +2841,57 @@ "typescript": { "status": "covered", "method": "MessagingClient.webhooks.list" - } - }, - { - "family": "messaging", - "method": "GET", + }, + "go": { + "status": "covered", + "method": "MessagingClient.Webhooks.List" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.webhooks.list" + }, + "php": { + "status": "covered", + "method": "MessagingClient::webhooks.list" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Webhooks.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.webhooks.list" + } + }, + { + "family": "messaging", + "method": "GET", "path": "/messaging/webhooks/{id}", "operationId": "getWebhook", "fingerprint": "9907bf3ee126a5be3bc6faa4354c6ebffbdca6cf9d7e5d8c2f678a03dd5ef96c", "typescript": { "status": "covered", "method": "MessagingClient.webhooks.retrieve" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Webhooks.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.webhooks.retrieve" + }, + "php": { + "status": "covered", + "method": "MessagingClient.webhooks.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Webhooks.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.webhooks.retrieve" } }, { @@ -1040,6 +2903,26 @@ "typescript": { "status": "covered", "method": "SystemClient.ping" + }, + "go": { + "status": "covered", + "method": "SystemClient.Ping" + }, + "python": { + "status": "covered", + "method": "AsyncSystemClient.ping" + }, + "php": { + "status": "covered", + "method": "SystemClient::ping" + }, + "dotnet": { + "status": "covered", + "method": "SystemClient.PingAsync" + }, + "rust": { + "status": "covered", + "method": "SystemClient.ping" } }, { @@ -1052,6 +2935,29 @@ "status": "excluded", "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", "milestone": "not-server-sdk" + }, + "go": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + "python": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" + }, + "php": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" + }, + "dotnet": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + "rust": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" } }, { @@ -1064,6 +2970,29 @@ "status": "excluded", "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", "milestone": "not-server-sdk" + }, + "go": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + "python": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" + }, + "php": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" + }, + "dotnet": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + "rust": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" } }, { @@ -1075,6 +3004,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.setCartEnabled" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.SetCartEnabled" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.set_cart_enabled" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->setCartEnabled" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.SetCartEnabledAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.set_cart_enabled" } }, { @@ -1086,6 +3035,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.updateCollection" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.UpdateCollection" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.update_collection" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->updateCollection" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.UpdateCollectionAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.update_collection" } }, { @@ -1097,6 +3066,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.setProductVisibility" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.SetProductVisibility" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.set_product_visibility" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->setProductVisibility" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.SetProductVisibilityAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.set_product_visibility" } }, { @@ -1108,6 +3097,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.updateProfile" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.UpdateProfile" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.update_profile" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->updateProfile" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.UpdateProfileAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.update_profile" } }, { @@ -1119,6 +3128,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.update" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.Update" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.update" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.update" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.update" } }, { @@ -1130,6 +3159,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.cloudTemplates.update" + }, + "go": { + "status": "covered", + "method": "MessagingClient.CloudTemplates.Update" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.cloud_templates.update" + }, + "php": { + "status": "covered", + "method": "MessagingClient::cloudTemplates.update" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.CloudTemplates.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.cloud_templates.update" } }, { @@ -1139,8 +3188,28 @@ "operationId": "updateProjectCampaign", "fingerprint": "4e4c02f896824deabf5c7742e1633ed4c6aa5615c9cbef502edbd02510e983ac", "typescript": { - "status": "excluded", - "reason": "Messaging campaign update is owned by SDK PR #307." + "status": "covered", + "method": "MessagingClient.campaigns.update" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.Update" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.update" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.update" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.update" } }, { @@ -1152,6 +3221,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.templates.update" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Templates.Update" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.templates.update" + }, + "php": { + "status": "covered", + "method": "MessagingClient::templates.update" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Templates.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.templates.update" } }, { @@ -1163,6 +3252,29 @@ "typescript": { "status": "excluded", "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "go": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "python": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "rust": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "not-server-sdk" } }, { @@ -1174,6 +3286,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.createCatalog" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.CreateCatalog" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.create_catalog" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->createCatalog" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.CreateCatalogAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.create_catalog" } }, { @@ -1185,6 +3317,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.createCollection" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.CreateCollection" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.create_collection" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->createCollection" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.CreateCollectionAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.create_collection" } }, { @@ -1196,6 +3348,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.appealCollection" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.AppealCollection" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.appeal_collection" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->appealCollection" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.AppealCollectionAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.appeal_collection" } }, { @@ -1207,6 +3379,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.reorderCollections" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.ReorderCollections" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.reorder_collections" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->reorderCollections" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.ReorderCollectionsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.reorder_collections" } }, { @@ -1218,6 +3410,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.getOrder" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.GetOrder" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.get_order" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->getOrder" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.GetOrderAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.get_order" } }, { @@ -1229,6 +3441,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.createProduct" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.CreateProduct" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.create_product" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->createProduct" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.CreateProductAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.create_product" } }, { @@ -1240,6 +3472,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.appealProduct" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.AppealProduct" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.appeal_product" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->appealProduct" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.AppealProductAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.appeal_product" } }, { @@ -1251,6 +3503,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.quickReplies.create" + }, + "go": { + "status": "covered", + "method": "MessagingClient.QuickReplies.Create" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.quick_replies.create" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$quickReplies->create" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.QuickReplies.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.quick_replies.create" } }, { @@ -1262,15 +3534,55 @@ "typescript": { "status": "covered", "method": "MessagingClient.calls.reject" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/messaging/{session}/channels", - "operationId": "createChannel", - "fingerprint": "4b8c8380a142c78d9638628f896bdc5bd39ccb4bcd9324471bc1bda320271401", - "typescript": { + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.Reject" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.calls.reject" + }, + "php": { + "status": "covered", + "method": "MessagingClient::calls.reject" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.RejectIncomingAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.calls.reject_incoming" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/channels", + "operationId": "createChannel", + "fingerprint": "4b8c8380a142c78d9638628f896bdc5bd39ccb4bcd9324471bc1bda320271401", + "typescript": { + "status": "covered", + "method": "MessagingClient.channels.create" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.Create" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.create" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->create" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.CreateAsync" + }, + "rust": { "status": "covered", "method": "MessagingClient.channels.create" } @@ -1284,6 +3596,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.follow" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.Follow" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.follow" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->follow" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.FollowAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.follow" } }, { @@ -1295,6 +3627,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.subscribeToLiveUpdates" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.SubscribeToLiveUpdates" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.subscribe_to_live_updates" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->subscribeToLiveUpdates" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.SubscribeToLiveUpdatesAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.subscribe_to_live_updates" } }, { @@ -1306,6 +3658,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.reactToMessage" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.ReactToMessage" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.react_to_message" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->reactToMessage" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.ReactToMessageAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.react_to_message" } }, { @@ -1317,6 +3689,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.markMessageViewed" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.MarkMessageViewed" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.mark_message_viewed" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->markMessageViewed" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.MarkMessageViewedAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.mark_message_viewed" } }, { @@ -1328,6 +3720,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.mute" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.Mute" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.mute" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->mute" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.MuteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.mute" } }, { @@ -1339,6 +3751,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.unfollow" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.Unfollow" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.unfollow" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->unfollow" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.UnfollowAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.unfollow" } }, { @@ -1350,6 +3782,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.channels.unmute" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Channels.Unmute" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.channels.unmute" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$channels->unmute" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Channels.UnmuteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.channels.unmute" } }, { @@ -1361,6 +3813,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.archive" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.Archive" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.archive" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->archive" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.ArchiveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.archive" } }, { @@ -1372,6 +3844,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.unarchive" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.Unarchive" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.unarchive" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->unarchive" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.UnarchiveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.unarchive" } }, { @@ -1383,6 +3875,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.sessions.reauthorizeCloudCredentials" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Sessions.ReauthorizeCloudCredentials" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.sessions.reauthorize_cloud_credentials" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$sessions->reauthorizeCloudCredentials" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Sessions.ReauthorizeCloudCredentialsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.reauthorize_cloud_credentials" } }, { @@ -1394,6 +3906,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.contacts.block" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Contacts.Block" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.contacts.block" + }, + "php": { + "status": "covered", + "method": "MessagingClient.contacts.block" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Contacts.BlockAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.contacts.block" } }, { @@ -1405,6 +3937,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.contacts.unblock" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Contacts.Unblock" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.contacts.unblock" + }, + "php": { + "status": "covered", + "method": "MessagingClient.contacts.unblock" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Contacts.UnblockAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.contacts.unblock" } }, { @@ -1416,6 +3968,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.create" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.Create" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.create" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.create" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.create" } }, { @@ -1427,6 +3999,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.demoteParticipants" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.DemoteParticipants" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.demote_participants" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.demoteParticipants" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.DemoteParticipantsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.demote_participants" } }, { @@ -1438,6 +4030,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.promoteParticipants" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.PromoteParticipants" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.promote_participants" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.promoteParticipants" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.PromoteParticipantsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.promote_participants" } }, { @@ -1449,6 +4061,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.revokeInviteCode" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.RevokeInviteCode" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.revoke_invite_code" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.revokeInviteCode" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.RevokeInviteCodeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.revoke_invite_code" } }, { @@ -1460,6 +4092,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.leave" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.Leave" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.leave" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.leave" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.LeaveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.leave" } }, { @@ -1471,6 +4123,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.addParticipants" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.AddParticipants" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.add_participants" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.addParticipants" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.AddParticipantsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.add_participants" } }, { @@ -1482,6 +4154,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.removeParticipants" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.RemoveParticipants" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.remove_participants" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.removeParticipants" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.RemoveParticipantsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.remove_participants" } }, { @@ -1493,6 +4185,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.join" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.Join" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.join" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.join" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.JoinAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.join" } }, { @@ -1504,17 +4216,57 @@ "typescript": { "status": "covered", "method": "MessagingClient.labels.create" - } - }, - { - "family": "messaging", - "method": "POST", + }, + "go": { + "status": "covered", + "method": "MessagingClient.Labels.Create" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.labels.create" + }, + "php": { + "status": "covered", + "method": "MessagingClient.labels.create" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Labels.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.labels.create" + } + }, + { + "family": "messaging", + "method": "POST", "path": "/messaging/{session}/messages/react", "operationId": "sendReaction", "fingerprint": "3aa8ea0864263c964b4c96a3291687990b3f4a815f1463eab6c2cbc6f9a69eb0", "typescript": { "status": "covered", "method": "MessagingClient.messages.react" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Messages.React" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.messages.react" + }, + "php": { + "status": "covered", + "method": "MessagingClient.messages.react" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Messages.ReactAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.messages.react" } }, { @@ -1526,6 +4278,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.messages.markSeen" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Messages.MarkSeen" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.messages.mark_seen" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$messages->markSeen" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Messages.MarkSeenAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.messages.mark_seen" } }, { @@ -1537,6 +4309,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.messages.send" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Messages.Send" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.messages.send" + }, + "php": { + "status": "covered", + "method": "MessagingClient::messages.send" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Messages.SendAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.messages.send" } }, { @@ -1548,6 +4340,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.messages.star" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Messages.Star" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.messages.star" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$messages->star" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Messages.StarAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.messages.star" } }, { @@ -1559,6 +4371,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.messages.setTyping" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Messages.SetTyping" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.messages.set_typing" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$messages->setTyping" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Messages.SetTypingAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.messages.set_typing" } }, { @@ -1570,6 +4402,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.create" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.Create" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.create" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.create" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.create" } }, { @@ -1581,6 +4433,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.resetInviteLink" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.ResetInviteLink" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.reset_invite_link" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.resetInviteLink" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.ResetInviteLinkAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.reset_invite_link" } }, { @@ -1592,6 +4464,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.approveJoinRequests" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.ApproveJoinRequests" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.approve_join_requests" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.approveJoinRequests" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.ApproveJoinRequestsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.approve_join_requests" } }, { @@ -1603,6 +4495,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.rejectJoinRequests" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.RejectJoinRequests" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.reject_join_requests" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.rejectJoinRequests" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.RejectJoinRequestsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.reject_join_requests" } }, { @@ -1614,6 +4526,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.removeParticipants" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.RemoveParticipants" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.remove_participants" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.removeParticipants" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.RemoveParticipantsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.remove_participants" } }, { @@ -1625,6 +4557,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.officialGroups.pin" + }, + "go": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.Pin" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.official_groups.pin" + }, + "php": { + "status": "covered", + "method": "MessagingClient::officialGroups.pin" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.OfficialGroups.PinAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.official_groups.pin" } }, { @@ -1636,6 +4588,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.sessions.requestPairingCode" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Sessions.RequestPairingCode" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.sessions.request_pairing_code" + }, + "php": { + "status": "covered", + "method": "MessagingClient.sessions.requestPairingCode" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Sessions.RequestPairingCodeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.request_pairing_code" } }, { @@ -1647,6 +4619,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.presence.set" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Presence.Set" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.presence.set" + }, + "php": { + "status": "covered", + "method": "MessagingClient.presence.set" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Presence.SetAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.presence.set" } }, { @@ -1658,6 +4650,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.presence.subscribe" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Presence.Subscribe" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.presence.subscribe" + }, + "php": { + "status": "covered", + "method": "MessagingClient.presence.subscribe" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Presence.SubscribeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.presence.subscribe" } }, { @@ -1669,6 +4681,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.cloudTemplates.create" + }, + "go": { + "status": "covered", + "method": "MessagingClient.CloudTemplates.Create" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.cloud_templates.create" + }, + "php": { + "status": "covered", + "method": "MessagingClient::cloudTemplates.create" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.CloudTemplates.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.cloud_templates.create" } }, { @@ -1680,6 +4712,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.cloudOnboarding.advance" + }, + "go": { + "status": "covered", + "method": "MessagingClient.CloudOnboarding.Advance" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.cloud_onboarding.advance" + }, + "php": { + "status": "covered", + "method": "MessagingClient::cloudOnboarding.advance" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.CloudOnboarding.AdvanceAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.cloud_onboarding.advance" } }, { @@ -1691,6 +4743,29 @@ "typescript": { "status": "excluded", "reason": "Host-runtime feature evaluation is not a customer messaging operation in this SDK." + }, + "go": { + "status": "excluded", + "reason": "Host-runtime feature evaluation is not a customer messaging operation in this SDK." + }, + "python": { + "status": "excluded", + "reason": "Host-runtime feature evaluation is not a customer messaging operation in this SDK.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Host-runtime feature evaluation is not a customer messaging operation in this SDK.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Host-runtime feature evaluation is not a customer messaging operation in this SDK." + }, + "rust": { + "status": "excluded", + "reason": "Host-runtime feature evaluation is not a customer messaging operation in this SDK.", + "milestone": "not-server-sdk" } }, { @@ -1702,6 +4777,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.media.persist" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Media.Persist" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.media.persist" + }, + "php": { + "status": "covered", + "method": "MessagingClient::media.persist" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Media.PersistAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.media.persist" } }, { @@ -1713,6 +4808,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.create" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.Create" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.create" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.create" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.create" } }, { @@ -1724,6 +4839,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.launch" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.Launch" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.launch" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.launch" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.LaunchAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.launch" } }, { @@ -1735,6 +4870,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.pause" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.Pause" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.pause" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.pause" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.PauseAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.pause" } }, { @@ -1746,6 +4901,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.addRecipients" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.AddRecipients" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.add_recipients" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.addRecipients" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.AddRecipientsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.add_recipients" } }, { @@ -1757,17 +4932,57 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.requeue" - } - }, - { + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.Requeue" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.requeue" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.requeue" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.RequeueAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.requeue" + } + }, + { "family": "messaging", "method": "POST", "path": "/messaging/projects/{projectSlug}/campaigns/{id}/reschedule", "operationId": "rescheduleCampaign", "fingerprint": "9b96f4321f98a3758c1b00b87a897bf5d52a48e78df99f40fab9324f6ec148ea", "typescript": { - "status": "excluded", - "reason": "Campaign reschedule is owned by SDK PR #310 and will be covered when that PR merges." + "status": "covered", + "method": "MessagingClient.campaigns.reschedule" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.Reschedule" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.reschedule" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.reschedule" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.RescheduleAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.reschedule" } }, { @@ -1779,6 +4994,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.resume" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.Resume" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.resume" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.resume" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.ResumeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.resume" } }, { @@ -1790,6 +5025,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.campaigns.stop" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Campaigns.Stop" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.campaigns.stop" + }, + "php": { + "status": "covered", + "method": "MessagingClient::campaigns.stop" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Campaigns.StopAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.campaigns.stop" } }, { @@ -1801,6 +5056,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.templates.create" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Templates.Create" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.templates.create" + }, + "php": { + "status": "covered", + "method": "MessagingClient::templates.create" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Templates.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.templates.create" } }, { @@ -1812,6 +5087,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.templates.preview" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Templates.Preview" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.templates.preview" + }, + "php": { + "status": "covered", + "method": "MessagingClient::templates.preview" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Templates.PreviewAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.templates.preview" } }, { @@ -1823,6 +5118,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.templates.submit" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Templates.Submit" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.templates.submit" + }, + "php": { + "status": "covered", + "method": "MessagingClient::templates.submit" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Templates.SubmitAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.templates.submit" } }, { @@ -1834,6 +5149,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.quickLinks.create" + }, + "go": { + "status": "covered", + "method": "MessagingClient.QuickLinks.Create" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.quick_links.create" + }, + "php": { + "status": "covered", + "method": "MessagingClient::quickLinks.create" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.QuickLinks.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.quick_links.create" } }, { @@ -1845,6 +5180,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.testing.triggerEvent" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Testing.TriggerEvent" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.testing.trigger_event" + }, + "php": { + "status": "covered", + "method": "MessagingClient::testing.triggerEvent" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Testing.TriggerEventAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.testing.trigger_event" } }, { @@ -1856,6 +5211,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.testing.createHistoryFixture" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Testing.CreateHistoryFixture" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.testing.create_history_fixture" + }, + "php": { + "status": "covered", + "method": "MessagingClient::testing.createHistoryFixture" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Testing.CreateHistoryFixtureAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.testing.create_history_fixture" } }, { @@ -1867,6 +5242,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.createCallLink" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.CreateCallLink" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.create_call_link" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->createCallLink" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.CreateCallLinkAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.create_call_link" } }, { @@ -1878,6 +5273,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.previewCallLink" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.PreviewCallLink" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.preview_call_link" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->previewCallLink" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.PreviewCallLinkAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.preview_call_link" } }, { @@ -1889,6 +5304,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.place" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.Place" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.place" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->place" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.PlaceAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.place" } }, { @@ -1900,6 +5335,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.accept" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.Accept" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.accept" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->accept" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.AcceptAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.accept" } }, { @@ -1911,6 +5366,29 @@ "typescript": { "status": "excluded", "reason": "used internally by the Calls SDK; not part of the public API" + }, + "go": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + "python": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + "rust": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "not-server-sdk" } }, { @@ -1922,6 +5400,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.setHandRaised" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.SetHandRaised" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.set_hand_raised" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->setHandRaised" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.SetHandRaisedAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.set_hand_raised" } }, { @@ -1933,6 +5431,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.leave" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.Leave" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.leave" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->leave" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.LeaveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.leave" } }, { @@ -1944,6 +5462,29 @@ "typescript": { "status": "excluded", "reason": "used internally by the Calls SDK; not part of the public API" + }, + "go": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + "python": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + "rust": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "not-server-sdk" } }, { @@ -1955,6 +5496,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.addParticipant" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.AddParticipant" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.add_participant" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->addParticipant" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.AddParticipantAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.add_participant" } }, { @@ -1966,6 +5527,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.ringParticipant" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.RingParticipant" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.ring_participant" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->ringParticipant" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.RingParticipantAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.ring_participant" } }, { @@ -1977,6 +5558,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.sendReaction" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.SendReaction" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.send_reaction" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->sendReaction" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.SendReactionAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.send_reaction" } }, { @@ -1988,6 +5589,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.reject" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.Reject" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.reject" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->reject" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.RejectAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.reject" } }, { @@ -1999,6 +5620,29 @@ "typescript": { "status": "excluded", "reason": "used internally by the Calls SDK; not part of the public API" + }, + "go": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + "python": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + "rust": { + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API", + "milestone": "not-server-sdk" } }, { @@ -2010,8 +5654,28 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.report" - } - }, + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.Report" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.report" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->report" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.ReportAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.report" + } + }, { "family": "messaging", "method": "POST", @@ -2021,6 +5685,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.check" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.Check" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.check" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->check" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Calls.CheckAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.check" } }, { @@ -2032,6 +5716,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.webhooks.create" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Webhooks.Create" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.webhooks.create" + }, + "php": { + "status": "covered", + "method": "MessagingClient::webhooks.create" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Webhooks.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.webhooks.create" } }, { @@ -2044,6 +5748,29 @@ "status": "excluded", "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", "milestone": "not-server-sdk" + }, + "go": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + "python": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" + }, + "php": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" + }, + "dotnet": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + "rust": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" } }, { @@ -2055,6 +5782,29 @@ "typescript": { "status": "excluded", "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "go": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "python": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "rust": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "not-server-sdk" } }, { @@ -2067,6 +5817,29 @@ "status": "excluded", "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", "milestone": "not-server-sdk" + }, + "go": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + "python": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" + }, + "php": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" + }, + "dotnet": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + "rust": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" } }, { @@ -2079,6 +5852,29 @@ "status": "excluded", "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", "milestone": "not-server-sdk" + }, + "go": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + "python": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" + }, + "php": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" + }, + "dotnet": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + "rust": { + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs.", + "milestone": "not-server-sdk" } }, { @@ -2090,6 +5886,29 @@ "typescript": { "status": "excluded", "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "go": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "python": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "rust": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "not-server-sdk" } }, { @@ -2101,6 +5920,29 @@ "typescript": { "status": "excluded", "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "go": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "python": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "rust": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "not-server-sdk" } }, { @@ -2112,6 +5954,29 @@ "typescript": { "status": "excluded", "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "go": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "python": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "rust": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "not-server-sdk" } }, { @@ -2123,6 +5988,29 @@ "typescript": { "status": "excluded", "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "go": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "python": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + "rust": { + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource.", + "milestone": "not-server-sdk" } }, { @@ -2134,6 +6022,29 @@ "typescript": { "status": "excluded", "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "go": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "python": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "rust": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "not-server-sdk" } }, { @@ -2145,6 +6056,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.setMerchantCompliance" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.SetMerchantCompliance" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.set_merchant_compliance" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->setMerchantCompliance" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.SetMerchantComplianceAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.set_merchant_compliance" } }, { @@ -2156,6 +6087,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.updateProduct" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.UpdateProduct" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.update_product" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->updateProduct" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.UpdateProductAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.update_product" } }, { @@ -2167,6 +6118,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.business.setCoverPhoto" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Business.SetCoverPhoto" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.business.set_cover_photo" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$business->setCoverPhoto" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Business.SetCoverPhotoAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.business.set_cover_photo" } }, { @@ -2178,6 +6149,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.quickReplies.replace" + }, + "go": { + "status": "covered", + "method": "MessagingClient.QuickReplies.Replace" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.quick_replies.replace" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$quickReplies->replace" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.QuickReplies.ReplaceAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.quick_replies.replace" } }, { @@ -2189,6 +6180,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.setDisappearingTimer" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.SetDisappearingTimer" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.set_disappearing_timer" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->setDisappearingTimer" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.SetDisappearingTimerAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.set_disappearing_timer" } }, { @@ -2200,6 +6211,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.chats.editMessage" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Chats.EditMessage" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.chats.edit_message" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$chats->editMessage" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Chats.EditMessageAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.chats.edit_message" } }, { @@ -2211,6 +6242,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.setDescription" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.SetDescription" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.set_description" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.setDescription" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.SetDescriptionAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.set_description" } }, { @@ -2222,6 +6273,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.setPicture" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.SetPicture" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.set_picture" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.setPicture" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.SetPictureAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.set_picture" } }, { @@ -2233,6 +6304,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.setInfoEditing" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.SetInfoEditing" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.set_info_editing" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.setInfoEditing" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.SetInfoEditingAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.set_info_editing" } }, { @@ -2244,8 +6335,28 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.setJoinApproval" - } - }, + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.SetJoinApproval" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.set_join_approval" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.setJoinApproval" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.SetJoinApprovalAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.set_join_approval" + } + }, { "family": "messaging", "method": "PUT", @@ -2255,6 +6366,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.setMemberAddMode" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.SetMemberAddMode" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.set_member_add_mode" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.setMemberAddMode" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.SetMemberAddModeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.set_member_add_mode" } }, { @@ -2266,6 +6397,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.setMessaging" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.SetMessaging" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.set_messaging" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.setMessaging" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.SetMessagingAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.set_messaging" } }, { @@ -2277,6 +6428,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.groups.setSubject" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Groups.SetSubject" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.groups.set_subject" + }, + "php": { + "status": "covered", + "method": "MessagingClient.groups.setSubject" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Groups.SetSubjectAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.groups.set_subject" } }, { @@ -2288,6 +6459,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.hybridLink.setPaused" + }, + "go": { + "status": "covered", + "method": "MessagingClient.HybridLink.SetPaused" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.hybrid_link.set_paused" + }, + "php": { + "status": "covered", + "method": "MessagingClient::hybridLink.setPaused" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.HybridLink.SetPausedAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.hybrid_link.set_paused" } }, { @@ -2299,6 +6490,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.labels.update" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Labels.Update" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.labels.update" + }, + "php": { + "status": "covered", + "method": "MessagingClient.labels.update" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Labels.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.labels.update" } }, { @@ -2310,6 +6521,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.labels.replaceForChat" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Labels.ReplaceForChat" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.labels.replace_for_chat" + }, + "php": { + "status": "covered", + "method": "MessagingClient.labels.replaceForChat" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Labels.ReplaceForChatAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.labels.replace_for_chat" } }, { @@ -2321,6 +6552,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.privacy.set" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Privacy.Set" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.privacy.set" + }, + "php": { + "status": "covered", + "method": "MessagingClient.privacy.set" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Privacy.SetAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.privacy.set" } }, { @@ -2332,6 +6583,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.privacy.setDefaultDisappearingTimer" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Privacy.SetDefaultDisappearingTimer" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.privacy.set_default_disappearing_timer" + }, + "php": { + "status": "covered", + "method": "MessagingClient.privacy.setDefaultDisappearingTimer" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Privacy.SetDefaultDisappearingTimerAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.privacy.set_default_disappearing_timer" } }, { @@ -2343,6 +6614,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.profile.setName" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Profile.SetName" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.profile.set_name" + }, + "php": { + "status": "covered", + "method": "MessagingClient.profile.setName" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Profile.SetNameAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.profile.set_name" } }, { @@ -2354,6 +6645,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.profile.setPicture" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Profile.SetPicture" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.profile.set_picture" + }, + "php": { + "status": "covered", + "method": "MessagingClient.profile.setPicture" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Profile.SetPictureAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.profile.set_picture" } }, { @@ -2365,6 +6676,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.profile.setStatus" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Profile.SetStatus" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.profile.set_status" + }, + "php": { + "status": "covered", + "method": "MessagingClient.profile.setStatus" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Profile.SetStatusAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.profile.set_status" } }, { @@ -2376,6 +6707,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.banSafe.updateSessionSafeMode" + }, + "go": { + "status": "covered", + "method": "MessagingClient.BanSafe.UpdateSessionSafeMode" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.ban_safe.update_session_safe_mode" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$banSafe->updateSessionSafeMode" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.BanSafe.UpdateSessionSafeModeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.ban_safe.update_session_safe_mode" } }, { @@ -2387,6 +6738,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.banSafe.updateProjectHealthPolicy" + }, + "go": { + "status": "covered", + "method": "MessagingClient.BanSafe.UpdateProjectHealthPolicy" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.ban_safe.update_project_health_policy" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$banSafe->updateProjectHealthPolicy" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.BanSafe.UpdateProjectHealthPolicyAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.ban_safe.update_project_health_policy" } }, { @@ -2398,6 +6769,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.banSafe.updateProjectInsuranceEvidence" + }, + "go": { + "status": "covered", + "method": "MessagingClient.BanSafe.UpdateProjectInsuranceEvidence" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.ban_safe.update_project_insurance_evidence" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$banSafe->updateProjectInsuranceEvidence" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.BanSafe.UpdateProjectInsuranceEvidenceAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.ban_safe.update_project_insurance_evidence" } }, { @@ -2409,6 +6800,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.banSafe.updateProjectSafeMode" + }, + "go": { + "status": "covered", + "method": "MessagingClient.BanSafe.UpdateProjectSafeMode" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.ban_safe.update_project_safe_mode" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$banSafe->updateProjectSafeMode" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.BanSafe.UpdateProjectSafeModeAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.ban_safe.update_project_safe_mode" } }, { @@ -2420,6 +6831,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.banSafe.updateProjectWarmupPlan" + }, + "go": { + "status": "covered", + "method": "MessagingClient.BanSafe.UpdateProjectWarmupPlan" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.ban_safe.update_project_warmup_plan" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$banSafe->updateProjectWarmupPlan" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.BanSafe.UpdateProjectWarmupPlanAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.ban_safe.update_project_warmup_plan" } }, { @@ -2431,6 +6862,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.hybridLink.setPolicy" + }, + "go": { + "status": "covered", + "method": "MessagingClient.HybridLink.SetPolicy" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.hybrid_link.set_policy" + }, + "php": { + "status": "covered", + "method": "MessagingClient::hybridLink.setPolicy" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.HybridLink.SetPolicyAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.hybrid_link.set_policy" } }, { @@ -2442,6 +6893,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.webhooks.update" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Webhooks.Update" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.webhooks.update" + }, + "php": { + "status": "covered", + "method": "MessagingClient::webhooks.update" + }, + "dotnet": { + "status": "covered", + "method": "MessagingClient.Webhooks.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.webhooks.update" } }, { @@ -2453,6 +6924,29 @@ "typescript": { "status": "excluded", "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "go": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "python": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "rust": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "not-server-sdk" } }, { @@ -2464,6 +6958,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2475,6 +6992,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2486,18 +7026,64 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." - } - }, - { - "family": "platform", - "method": "DELETE", - "path": "/console/calls/{callId}", - "operationId": "terminateCall", - "fingerprint": "313a0df22239dc66881fc4d22671d3e04aad2a36a84bf491d5d94f37e1d1074a", - "typescript": { + }, + "go": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." - } + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" + } + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/calls/{callId}", + "operationId": "terminateCall", + "fingerprint": "313a0df22239dc66881fc4d22671d3e04aad2a36a84bf491d5d94f37e1d1074a", + "typescript": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" + } }, { "family": "platform", @@ -2508,6 +7094,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2519,6 +7128,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2530,6 +7162,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2541,6 +7196,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2552,6 +7230,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2563,6 +7264,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -2574,6 +7298,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -2585,6 +7332,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2596,6 +7366,26 @@ "typescript": { "status": "covered", "method": "Client.audiences.delete" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Audiences.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.audiences.delete" + }, + "php": { + "status": "covered", + "method": "Client::audiences.delete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Audiences.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.audiences.delete" } }, { @@ -2607,6 +7397,26 @@ "typescript": { "status": "covered", "method": "Client.audiences.deleteMember" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Audiences.DeleteMember" + }, + "python": { + "status": "covered", + "method": "AsyncClient.audiences.delete_member" + }, + "php": { + "status": "covered", + "method": "Client::audiences.deleteMember" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Audiences.DeleteMemberAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.audiences.delete_member" } }, { @@ -2618,6 +7428,26 @@ "typescript": { "status": "covered", "method": "Client.callOptOuts.delete" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.CallOptOuts.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.call_opt_outs.delete" + }, + "php": { + "status": "covered", + "method": "Client::callOptOuts.delete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.CallOptOuts.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.call_opt_outs.delete" } }, { @@ -2629,6 +7459,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.delete" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.delete" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.delete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.delete" } }, { @@ -2640,6 +7490,26 @@ "typescript": { "status": "covered", "method": "Client.customers.revokePairingLink" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.RevokePairingLink" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.revoke_pairing_link" + }, + "php": { + "status": "covered", + "method": "Client::customers.revokePairingLink" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.RevokePairingLinkAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.revoke_pairing_link" } }, { @@ -2651,6 +7521,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.delete" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.delete" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.delete" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.delete" } }, { @@ -2662,6 +7552,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.deleteEndpoint" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.DeleteEndpoint" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.delete_endpoint" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.deleteEndpoint" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.DeleteEndpointAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.delete_endpoint" } }, { @@ -2673,6 +7583,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.delete" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.delete" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.delete" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.delete" } }, { @@ -2684,6 +7614,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.secrets.revoke" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Secrets.Revoke" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.secrets.revoke" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.secrets.revoke" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.Secrets.RevokeAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.secrets.revoke" } }, { @@ -2695,6 +7645,26 @@ "typescript": { "status": "covered", "method": "Client.apiKeys.deactivate" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.APIKeys.Deactivate" + }, + "python": { + "status": "covered", + "method": "AsyncClient.api_keys.deactivate" + }, + "php": { + "status": "covered", + "method": "Client::apiKeys.deactivate" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.ApiKeys.DeactivateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.api_keys.deactivate" } }, { @@ -2706,6 +7676,26 @@ "typescript": { "status": "covered", "method": "Client.media.delete" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Media.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.media.delete" + }, + "php": { + "status": "covered", + "method": "Client::media.delete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Media.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.media.delete" } }, { @@ -2717,17 +7707,57 @@ "typescript": { "status": "covered", "method": "Client.optOuts.delete" - } - }, - { - "family": "platform", - "method": "DELETE", - "path": "/platform/projects/{projectId}/webhooks/{webhookId}", - "operationId": "deleteProjectWebhook", + }, + "go": { + "status": "covered", + "method": "OrganizationClient.OptOuts.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.opt_outs.delete" + }, + "php": { + "status": "covered", + "method": "Client::optOuts.delete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.OptOuts.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.opt_outs.delete" + } + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/projects/{projectId}/webhooks/{webhookId}", + "operationId": "deleteProjectWebhook", "fingerprint": "492640b9ce2b5310a460426d1b944e4060efa443b05b34a99c455d127df18b7f", "typescript": { "status": "covered", "method": "Client.project(projectId).webhooks.delete" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Webhooks.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhooks.delete" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.delete" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Webhooks.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhooks.delete" } }, { @@ -2739,6 +7769,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.clientTokens.deleteRules" + }, + "go": { + "status": "covered", + "method": "MessagingClient.ClientTokens.DeleteRules" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.client_tokens.delete_rules" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$clientTokens->deleteRules" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.ClientTokens.DeleteRulesAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.client_tokens.delete_rules" } }, { @@ -2750,6 +7800,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.delete" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.delete" + }, + "php": { + "status": "covered", + "method": "Client::sessions.delete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.delete" } }, { @@ -2761,6 +7831,26 @@ "typescript": { "status": "covered", "method": "Client.sipTrunks.delete" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SIPTrunks.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sip_trunks.delete" + }, + "php": { + "status": "covered", + "method": "Client::sipTrunks.delete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SipTrunks.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sip_trunks.delete" } }, { @@ -2773,6 +7863,29 @@ "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by Client.", "milestone": "credential-boundary" + }, + "go": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client." + }, + "python": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" + }, + "php": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" + }, + "dotnet": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client." + }, + "rust": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" } }, { @@ -2784,6 +7897,26 @@ "typescript": { "status": "covered", "method": "Client.voice.audio.delete" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.audio.delete" + }, + "php": { + "status": "covered", + "method": "Client::voice.audio.delete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.audio.delete" } }, { @@ -2795,6 +7928,26 @@ "typescript": { "status": "covered", "method": "Client.voice.providerCredentials.delete" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.ProviderCredentials.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.provider_credentials.delete" + }, + "php": { + "status": "covered", + "method": "Client::voice.providerCredentials.delete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.ProviderCredentials.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.provider_credentials.delete" } }, { @@ -2806,6 +7959,26 @@ "typescript": { "status": "covered", "method": "Client.webhooks.delete" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Webhooks.Delete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhooks.delete" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.delete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Webhooks.DeleteAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhooks.delete" } }, { @@ -2817,6 +7990,29 @@ "typescript": { "status": "excluded", "reason": "Console-only redacted account health requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only redacted account health requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only redacted account health requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only redacted account health requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only redacted account health requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only redacted account health requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2828,6 +8024,29 @@ "typescript": { "status": "excluded", "reason": "Console-only redacted account health export requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only redacted account health export requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only redacted account health export requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only redacted account health export requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only redacted account health export requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only redacted account health export requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2839,6 +8058,29 @@ "typescript": { "status": "excluded", "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "go": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "python": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "rust": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "not-server-sdk" } }, { @@ -2850,6 +8092,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "not-server-sdk" } }, { @@ -2861,6 +8126,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2872,6 +8160,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2883,6 +8194,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2894,6 +8228,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2905,6 +8262,29 @@ "typescript": { "status": "excluded", "reason": "Console-only dashboard operation; not a public SDK method." + }, + "go": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method." + }, + "python": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method." + }, + "rust": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method.", + "milestone": "not-server-sdk" } }, { @@ -2916,6 +8296,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2927,6 +8330,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2938,17 +8364,63 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." - } - }, - { - "family": "platform", - "method": "GET", - "path": "/console/deployments", - "operationId": "listDeployments", - "fingerprint": "4d5feff3ce43c8f3145ae8cae5cb69c97abb953e5ba06245fcc855b2e6f236ce", - "typescript": { + }, + "go": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/console/deployments", + "operationId": "listDeployments", + "fingerprint": "4d5feff3ce43c8f3145ae8cae5cb69c97abb953e5ba06245fcc855b2e6f236ce", + "typescript": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2960,6 +8432,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2971,6 +8466,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "not-server-sdk" } }, { @@ -2982,6 +8500,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -2993,6 +8534,29 @@ "typescript": { "status": "excluded", "reason": "Private Console selector requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "go": { + "status": "excluded", + "reason": "Private Console selector requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "python": { + "status": "excluded", + "reason": "Private Console selector requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Private Console selector requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Private Console selector requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "rust": { + "status": "excluded", + "reason": "Private Console selector requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "not-server-sdk" } }, { @@ -3004,6 +8568,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3015,6 +8602,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3026,6 +8636,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3037,6 +8670,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3048,6 +8704,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3059,6 +8738,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3070,6 +8772,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3081,6 +8806,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3092,6 +8840,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "not-server-sdk" } }, { @@ -3103,6 +8874,29 @@ "typescript": { "status": "excluded", "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3114,6 +8908,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3125,6 +8942,29 @@ "typescript": { "status": "excluded", "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "go": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "python": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "rust": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "not-server-sdk" } }, { @@ -3136,6 +8976,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3147,7 +9010,30 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." - } + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" + } }, { "family": "platform", @@ -3158,6 +9044,29 @@ "typescript": { "status": "excluded", "reason": "Console-only dashboard operation; not a public SDK method." + }, + "go": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method." + }, + "python": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method." + }, + "rust": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method.", + "milestone": "not-server-sdk" } }, { @@ -3169,6 +9078,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3180,6 +9112,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -3191,6 +9146,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -3202,6 +9180,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -3213,6 +9214,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -3224,6 +9248,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -3235,6 +9282,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -3246,6 +9316,26 @@ "typescript": { "status": "covered", "method": "Client.audiences.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Audiences.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.audiences.list" + }, + "php": { + "status": "covered", + "method": "Client::audiences.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Audiences.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.audiences.list" } }, { @@ -3257,6 +9347,26 @@ "typescript": { "status": "covered", "method": "Client.audiences.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Audiences.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.audiences.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::audiences.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Audiences.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.audiences.retrieve" } }, { @@ -3268,6 +9378,26 @@ "typescript": { "status": "covered", "method": "Client.audiences.listMembers" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Audiences.ListMembers" + }, + "python": { + "status": "covered", + "method": "AsyncClient.audiences.list_members" + }, + "php": { + "status": "covered", + "method": "Client::audiences.listMembers" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Audiences.ListMembersAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.audiences.list_members" } }, { @@ -3279,6 +9409,26 @@ "typescript": { "status": "covered", "method": "Client.auditLogs.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.AuditLogs.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.audit_logs.list" + }, + "php": { + "status": "covered", + "method": "Client::auditLogs.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.AuditLogs.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.audit_logs.list" } }, { @@ -3290,6 +9440,26 @@ "typescript": { "status": "covered", "method": "Client.sessionBans.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SessionBans.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.session_bans.list" + }, + "php": { + "status": "covered", + "method": "Client::sessionBans.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SessionBans.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.session_bans.list" } }, { @@ -3301,6 +9471,26 @@ "typescript": { "status": "covered", "method": "Client.sessionBans.listActive" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SessionBans.ListActive" + }, + "python": { + "status": "covered", + "method": "AsyncClient.session_bans.list_active" + }, + "php": { + "status": "covered", + "method": "Client::sessionBans.listActive" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SessionBans.ListActiveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.session_bans.list_active" } }, { @@ -3312,6 +9502,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.listClaims" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListClaims" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.list_claims" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.listClaims" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListClaimsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.list_claims" } }, { @@ -3323,6 +9533,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.getClaim" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.GetClaim" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.get_claim" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.getClaim" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.GetClaimAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.get_claim" } }, { @@ -3334,6 +9564,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.listCollection" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListCollection" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.list_collection" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.listCollection" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListCollectionAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.list_collection" } }, { @@ -3345,6 +9595,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.listEnforcement" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListEnforcement" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.list_enforcement" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.listEnforcement" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListEnforcementAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.list_enforcement" } }, { @@ -3356,6 +9626,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.listFindings" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListFindings" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.list_findings" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.listFindings" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListFindingsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.list_findings" } }, { @@ -3367,6 +9657,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.listHealth" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListHealth" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.list_health" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.listHealth" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListHealthAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.list_health" } }, { @@ -3378,6 +9688,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.listHealthActions" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListHealthActions" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.list_health_actions" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.listHealthActions" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListHealthActionsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.list_health_actions" } }, { @@ -3389,17 +9719,57 @@ "typescript": { "status": "covered", "method": "Client.banSafe.getHealth" - } - }, - { - "family": "platform", - "method": "GET", - "path": "/platform/bansafe/health/{session}/history", - "operationId": "listBanSafeHealthHistory", - "fingerprint": "f88529ced42278d1f57791261be675f054f0e31522799f15caa4d3b3efea4911", + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.GetHealth" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.get_health" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.getHealth" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.GetHealthAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.get_health" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/health/{session}/history", + "operationId": "listBanSafeHealthHistory", + "fingerprint": "f88529ced42278d1f57791261be675f054f0e31522799f15caa4d3b3efea4911", "typescript": { "status": "covered", "method": "Client.banSafe.listHealthHistory" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListHealthHistory" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.list_health_history" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.listHealthHistory" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListHealthHistoryAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.list_health_history" } }, { @@ -3411,6 +9781,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.listIncidents" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListIncidents" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.list_incidents" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.listIncidents" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListIncidentsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.list_incidents" } }, { @@ -3422,6 +9812,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.listSignals" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListSignals" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.list_signals" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.listSignals" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListSignalsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.list_signals" } }, { @@ -3433,6 +9843,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.getTelemetry" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.GetTelemetry" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.get_telemetry" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.getTelemetry" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.GetTelemetryAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.get_telemetry" } }, { @@ -3444,6 +9874,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.listTelemetryHistory" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListTelemetryHistory" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.list_telemetry_history" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.listTelemetryHistory" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.ListTelemetryHistoryAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.list_telemetry_history" } }, { @@ -3455,6 +9905,26 @@ "typescript": { "status": "covered", "method": "Client.billing.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::billing.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.retrieve" } }, { @@ -3466,6 +9936,26 @@ "typescript": { "status": "covered", "method": "Client.billing.getResourceControls" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.GetResourceControls" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.get_resource_controls" + }, + "php": { + "status": "covered", + "method": "Client::billing.getResourceControls" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.GetResourceControlsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.get_resource_controls" } }, { @@ -3477,6 +9967,26 @@ "typescript": { "status": "covered", "method": "Client.billing.getLimits" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.GetLimits" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.get_limits" + }, + "php": { + "status": "covered", + "method": "Client::billing.getLimits" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.GetLimitsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.get_limits" } }, { @@ -3488,6 +9998,26 @@ "typescript": { "status": "covered", "method": "Client.billing.listPricing" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.ListPricing" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.list_pricing" + }, + "php": { + "status": "covered", + "method": "Client::billing.listPricing" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.ListPricingAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.list_pricing" } }, { @@ -3499,6 +10029,26 @@ "typescript": { "status": "covered", "method": "Client.billing.getPriorities" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.GetPriorities" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.get_priorities" + }, + "php": { + "status": "covered", + "method": "Client::billing.getPriorities" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.GetPrioritiesAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.get_priorities" } }, { @@ -3510,6 +10060,26 @@ "typescript": { "status": "covered", "method": "Client.billing.listTransactions" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.ListTransactions" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.list_transactions" + }, + "php": { + "status": "covered", + "method": "Client::billing.listTransactions" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.ListTransactionsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.list_transactions" } }, { @@ -3521,6 +10091,26 @@ "typescript": { "status": "covered", "method": "Client.billing.usage" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.Usage" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.usage" + }, + "php": { + "status": "covered", + "method": "Client::billing.usage" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.UsageAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.usage" } }, { @@ -3532,6 +10122,26 @@ "typescript": { "status": "covered", "method": "Client.callOptOuts.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.CallOptOuts.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.call_opt_outs.list" + }, + "php": { + "status": "covered", + "method": "Client::callOptOuts.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.CallOptOuts.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.call_opt_outs.list" } }, { @@ -3543,6 +10153,26 @@ "typescript": { "status": "covered", "method": "Client.callPolicy.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.CallPolicy.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.call_policy.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::callPolicy.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.CallPolicy.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.call_policy.retrieve" } }, { @@ -3554,6 +10184,26 @@ "typescript": { "status": "covered", "method": "Client.callRetention.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.CallRetention.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.call_retention.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::callRetention.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.CallRetention.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.call_retention.retrieve" } }, { @@ -3565,6 +10215,26 @@ "typescript": { "status": "covered", "method": "Client.calls.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Calls.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.calls.list" + }, + "php": { + "status": "covered", + "method": "Client::calls.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Calls.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.calls.list" } }, { @@ -3576,6 +10246,26 @@ "typescript": { "status": "covered", "method": "Client.calls.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Calls.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.calls.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::calls.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Calls.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.calls.retrieve" } }, { @@ -3587,6 +10277,26 @@ "typescript": { "status": "covered", "method": "Client.calls.export" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Calls.Export" + }, + "python": { + "status": "covered", + "method": "AsyncClient.calls.export" + }, + "php": { + "status": "covered", + "method": "Client::calls.export" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Calls.ExportAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.calls.export" } }, { @@ -3598,6 +10308,26 @@ "typescript": { "status": "covered", "method": "Client.calls.stats" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Calls.Stats" + }, + "python": { + "status": "covered", + "method": "AsyncClient.calls.stats" + }, + "php": { + "status": "covered", + "method": "Client::calls.stats" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Calls.StatsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.calls.stats" } }, { @@ -3609,6 +10339,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.list" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.list" } }, { @@ -3620,6 +10370,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.retrieve" } }, { @@ -3631,6 +10401,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.analytics" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Analytics" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.analytics" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.analytics" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.AnalyticsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.analytics" } }, { @@ -3642,6 +10432,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.conversions" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Conversions" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.conversions" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.conversions" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.ConversionsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.conversions" } }, { @@ -3653,17 +10463,57 @@ "typescript": { "status": "covered", "method": "Client.campaigns.events" - } - }, - { - "family": "platform", - "method": "GET", - "path": "/platform/campaigns/{campaignId}/recipients", - "operationId": "listCampaignRecipients", - "fingerprint": "3fc27f45197d97447db90e3f4344e2c600aabbfc30746b9b61bd552472442505", + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Events" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.events" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.events" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.EventsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.events" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/campaigns/{campaignId}/recipients", + "operationId": "listCampaignRecipients", + "fingerprint": "3fc27f45197d97447db90e3f4344e2c600aabbfc30746b9b61bd552472442505", "typescript": { "status": "covered", "method": "Client.campaigns.recipients" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Recipients" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.recipients" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.recipients" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.RecipientsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.recipients" } }, { @@ -3675,6 +10525,26 @@ "typescript": { "status": "covered", "method": "Client.customers.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.list" + }, + "php": { + "status": "covered", + "method": "Client::customers.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.list" } }, { @@ -3686,6 +10556,26 @@ "typescript": { "status": "covered", "method": "Client.customers.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::customers.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.retrieve" } }, { @@ -3697,6 +10587,26 @@ "typescript": { "status": "covered", "method": "Client.customers.listEvents" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.ListEvents" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.list_events" + }, + "php": { + "status": "covered", + "method": "Client::customers.listEvents" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.ListEventsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.list_events" } }, { @@ -3708,6 +10618,26 @@ "typescript": { "status": "covered", "method": "Client.customers.listNumbers" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.ListNumbers" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.list_numbers" + }, + "php": { + "status": "covered", + "method": "Client::customers.listNumbers" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.ListNumbersAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.list_numbers" } }, { @@ -3719,6 +10649,26 @@ "typescript": { "status": "covered", "method": "Client.customers.listPairingLinks" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.ListPairingLinks" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.list_pairing_links" + }, + "php": { + "status": "covered", + "method": "Client::customers.listPairingLinks" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.ListPairingLinksAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.list_pairing_links" } }, { @@ -3730,6 +10680,26 @@ "typescript": { "status": "covered", "method": "Client.events.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Events.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.events.list" + }, + "php": { + "status": "covered", + "method": "Client::events.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Events.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.events.list" } }, { @@ -3741,6 +10711,26 @@ "typescript": { "status": "covered", "method": "Client.events.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Events.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.events.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::events.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Events.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.events.retrieve_with_payload" } }, { @@ -3752,6 +10742,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.encryptionKey" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.EncryptionKey" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.encryption_key" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.encryptionKey" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.EncryptionKeyAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.encryption_key" } }, { @@ -3763,6 +10773,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.list" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.List" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.list" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.list" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.ListAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.list" } }, { @@ -3774,6 +10804,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.retrieve" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.retrieve" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.retrieve" } }, { @@ -3785,6 +10835,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.endpoint" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Endpoint" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.endpoint" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.endpoint" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.EndpointAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.endpoint" } }, { @@ -3796,6 +10866,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.endpointReceipts" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.EndpointReceipts" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.endpoint_receipts" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.endpointReceipts" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.EndpointReceiptsAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.endpoint_receipts" } }, { @@ -3807,6 +10897,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.receipts" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Receipts" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.receipts" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.receipts" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.ReceiptsAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.receipts" } }, { @@ -3818,6 +10928,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.list" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.List" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.list" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.list" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.ListAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.list" } }, { @@ -3829,6 +10959,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.retrieve" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.retrieve" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.retrieve" } }, { @@ -3840,6 +10990,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.deployments.list" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Deployments.List" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.deployments.list" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.deployments.list" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.Deployments.ListAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.deployments.list" } }, { @@ -3851,6 +11021,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.deployments.retrieve" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Deployments.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.deployments.retrieve" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.deployments.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.Deployments.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.deployments.retrieve" } }, { @@ -3862,6 +11052,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.invocations.list" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Invocations.List" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.invocations.list" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.invocations.list" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.Invocations.ListAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.invocations.list" } }, { @@ -3873,6 +11083,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.invocations.retrieve" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Invocations.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.invocations.retrieve" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.invocations.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.Invocations.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.invocations.retrieve" } }, { @@ -3884,6 +11114,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.secrets.list" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Secrets.List" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.secrets.list" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.secrets.list" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.Secrets.ListAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.secrets.list" } }, { @@ -3895,6 +11145,26 @@ "typescript": { "status": "covered", "method": "Client.usage.listGates" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Usage.ListGates" + }, + "python": { + "status": "covered", + "method": "AsyncClient.usage.list_gates" + }, + "php": { + "status": "covered", + "method": "Client::usage.listGates" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Usage.ListGatesAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.usage.list_gates" } }, { @@ -3906,6 +11176,26 @@ "typescript": { "status": "covered", "method": "Client.securityIncidents.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SecurityIncidents.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.security_incidents.list" + }, + "php": { + "status": "covered", + "method": "Client::securityIncidents.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SecurityIncidents.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.security_incidents.list" } }, { @@ -3917,17 +11207,57 @@ "typescript": { "status": "covered", "method": "Client.apiKeys.list" - } - }, - { - "family": "platform", - "method": "GET", - "path": "/platform/media/{mediaId}", - "operationId": "getMediaUrl", - "fingerprint": "6320d2cd1af9d90a8dac77041c2ae7535594f95da19a378ffc01f6d88229cdc9", + }, + "go": { + "status": "covered", + "method": "OrganizationClient.APIKeys.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.api_keys.list" + }, + "php": { + "status": "covered", + "method": "Client::apiKeys.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.ApiKeys.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.api_keys.list" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/media/{mediaId}", + "operationId": "getMediaUrl", + "fingerprint": "6320d2cd1af9d90a8dac77041c2ae7535594f95da19a378ffc01f6d88229cdc9", "typescript": { "status": "covered", "method": "Client.media.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Media.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.media.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::media.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Media.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.media.retrieve" } }, { @@ -3939,6 +11269,26 @@ "typescript": { "status": "covered", "method": "Client.members.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Members.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.members.list" + }, + "php": { + "status": "covered", + "method": "Client::members.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Members.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.members.list" } }, { @@ -3950,6 +11300,26 @@ "typescript": { "status": "covered", "method": "Client.operations.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Operations.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.operations.list" + }, + "php": { + "status": "covered", + "method": "Client::operations.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Operations.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.operations.list" } }, { @@ -3961,6 +11331,26 @@ "typescript": { "status": "covered", "method": "Client.operations.get" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Operations.Get" + }, + "python": { + "status": "covered", + "method": "AsyncClient.operations.get" + }, + "php": { + "status": "covered", + "method": "Client::operations.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Operations.GetAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.operations.get" } }, { @@ -3972,6 +11362,26 @@ "typescript": { "status": "covered", "method": "Client.operations.listTransitions" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Operations.ListTransitions" + }, + "python": { + "status": "covered", + "method": "AsyncClient.operations.list_transitions" + }, + "php": { + "status": "covered", + "method": "Client::operations.listTransitions" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Operations.ListTransitionsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.operations.list_transitions" } }, { @@ -3983,6 +11393,26 @@ "typescript": { "status": "covered", "method": "Client.optOuts.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.OptOuts.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.opt_outs.list" + }, + "php": { + "status": "covered", + "method": "Client::optOuts.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.OptOuts.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.opt_outs.list" } }, { @@ -3994,6 +11424,26 @@ "typescript": { "status": "covered", "method": "Client.optOuts.getSettings" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.OptOuts.GetSettings" + }, + "python": { + "status": "covered", + "method": "AsyncClient.opt_outs.get_settings" + }, + "php": { + "status": "covered", + "method": "Client::optOuts.getSettings" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.OptOuts.GetSettingsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.opt_outs.get_settings" } }, { @@ -4005,6 +11455,26 @@ "typescript": { "status": "covered", "method": "Client.projects.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.list" + }, + "php": { + "status": "covered", + "method": "Client::projects.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.list" } }, { @@ -4016,6 +11486,26 @@ "typescript": { "status": "covered", "method": "Client.customers.status" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.Status" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.status" + }, + "php": { + "status": "covered", + "method": "Client::customers.status" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.StatusAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.status" } }, { @@ -4027,6 +11517,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).events.list" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Events.List" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.events.list" + }, + "php": { + "status": "covered", + "method": "Client::events.list" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Events.ListAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.events.list" } }, { @@ -4038,6 +11548,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).events.retrieve" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Events.Stream" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.events.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::events.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Events.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.events.retrieve_with_payload" } }, { @@ -4049,6 +11579,26 @@ "typescript": { "status": "covered", "method": "Client.events.stream" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Events.Stream" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.events.stream" + }, + "php": { + "status": "covered", + "method": "Client::events.stream" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Events.Stream" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.events.stream" } }, { @@ -4060,6 +11610,26 @@ "typescript": { "status": "covered", "method": "Client.projects.getHealthPolicy" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.GetHealthPolicy" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.get_health_policy" + }, + "php": { + "status": "covered", + "method": "Client::$projects->getHealthPolicy" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.GetHealthPolicyAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.get_health_policy" } }, { @@ -4071,6 +11641,26 @@ "typescript": { "status": "covered", "method": "Client.projects.listHybridMergeCandidates" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.ListHybridMergeCandidates" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.list_hybrid_merge_candidates" + }, + "php": { + "status": "covered", + "method": "Client::projects.listHybridMergeCandidates" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.ListHybridMergeCandidatesAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.list_hybrid_merge_candidates" } }, { @@ -4082,6 +11672,26 @@ "typescript": { "status": "covered", "method": "Client.projects.getInsuranceEvidence" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.GetInsuranceEvidence" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.get_insurance_evidence" + }, + "php": { + "status": "covered", + "method": "Client::$projects->getInsuranceEvidence" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.GetInsuranceEvidenceAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.get_insurance_evidence" } }, { @@ -4093,6 +11703,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).operations.list" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Operations.List" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.operations.list" + }, + "php": { + "status": "covered", + "method": "Client::operations.list" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Operations.ListAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.operations.list" } }, { @@ -4104,6 +11734,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).operations.get" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Operations.Get" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.operations.get" + }, + "php": { + "status": "covered", + "method": "Client::operations.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Operations.GetAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.operations.get" } }, { @@ -4115,6 +11765,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).operations.listTransitions" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Operations.ListTransitions" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.operations.list_transitions" + }, + "php": { + "status": "covered", + "method": "Client::operations.listTransitions" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Operations.ListTransitionsAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.operations.list_transitions" } }, { @@ -4126,6 +11796,26 @@ "typescript": { "status": "covered", "method": "Client.projects.getSafeMode" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.GetSafeMode" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.get_safe_mode" + }, + "php": { + "status": "covered", + "method": "Client::$projects->getSafeMode" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.GetSafeModeAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.get_safe_mode" } }, { @@ -4137,6 +11827,26 @@ "typescript": { "status": "covered", "method": "Client.projects.getWarmupPlan" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.GetWarmupPlan" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.get_warmup_plan" + }, + "php": { + "status": "covered", + "method": "Client::$projects->getWarmupPlan" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.GetWarmupPlanAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.get_warmup_plan" } }, { @@ -4148,6 +11858,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhookDeliveries.list" + }, + "go": { + "status": "covered", + "method": "ProjectClient.WebhookDeliveries.List" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhook_deliveries.list" + }, + "php": { + "status": "covered", + "method": "Client::webhookDeliveries.list" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.WebhookDeliveries.ListAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhook_deliveries.list" } }, { @@ -4159,6 +11889,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhookDeliveries.retrieve" + }, + "go": { + "status": "covered", + "method": "ProjectClient.WebhookDeliveries.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhook_deliveries.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::webhookDeliveries.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.WebhookDeliveries.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhook_deliveries.retrieve" } }, { @@ -4170,6 +11920,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhookDeliveries.listAttempts" + }, + "go": { + "status": "covered", + "method": "ProjectClient.WebhookDeliveries.ListAttempts" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhook_deliveries.list_attempts" + }, + "php": { + "status": "covered", + "method": "Client::webhookDeliveries.listAttempts" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.WebhookDeliveries.ListAttemptsAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhook_deliveries.list_attempts" } }, { @@ -4181,17 +11951,57 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhookDeliveries.retrieveAttempt" - } - }, - { - "family": "platform", - "method": "GET", - "path": "/platform/projects/{projectId}/webhooks", - "operationId": "listProjectWebhooks", - "fingerprint": "3ab481886083889991074bbb5ba0f32ce0ac36c928b0ae7458a480024629acf6", + }, + "go": { + "status": "covered", + "method": "ProjectClient.WebhookDeliveries.RetrieveAttempt" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhook_deliveries.retrieve_attempt" + }, + "php": { + "status": "covered", + "method": "Client::webhookDeliveries.retrieveAttempt" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.WebhookDeliveries.RetrieveAttemptAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhook_deliveries.retrieve_attempt" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/webhooks", + "operationId": "listProjectWebhooks", + "fingerprint": "3ab481886083889991074bbb5ba0f32ce0ac36c928b0ae7458a480024629acf6", "typescript": { "status": "covered", "method": "Client.project(projectId).webhooks.list" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Webhooks.List" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhooks.list" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.list" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Webhooks.ListAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhooks.list" } }, { @@ -4203,6 +12013,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhooks.retrieve" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Webhooks.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhooks.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Webhooks.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhooks.retrieve" } }, { @@ -4214,6 +12044,26 @@ "typescript": { "status": "covered", "method": "Client.quickLinkSettings.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.QuickLinkSettings.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.quick_link_settings.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::quickLinkSettings.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.QuickLinkSettings.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.quick_link_settings.retrieve" } }, { @@ -4225,6 +12075,26 @@ "typescript": { "status": "covered", "method": "Client.sessionConfiguration.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SessionConfiguration.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.session_configuration.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::sessionConfiguration.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Configuration.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.session_configuration.retrieve" } }, { @@ -4236,6 +12106,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.list" + }, + "php": { + "status": "covered", + "method": "Client::sessions.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.ListAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.list" } }, { @@ -4247,6 +12137,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.retrieveCallSettings" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.RetrieveCallSettings" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.retrieve_call_settings" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->retrieveCallSettings" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Calls.RetrieveCallSettingsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.retrieve_call_settings" } }, { @@ -4258,6 +12168,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.clientTokens.retrieveRules" + }, + "go": { + "status": "covered", + "method": "MessagingClient.ClientTokens.RetrieveRules" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.client_tokens.retrieve_rules" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$clientTokens->retrieveRules" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.ClientTokens.RetrieveRulesAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.client_tokens.retrieve_rules" } }, { @@ -4269,6 +12199,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::sessions.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.retrieve" } }, { @@ -4280,6 +12230,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.getCapabilities" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.GetCapabilities" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.get_capabilities" + }, + "php": { + "status": "covered", + "method": "Client::sessions.getCapabilities" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.GetCapabilitiesAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sessions.get_capabilities" } }, { @@ -4291,6 +12261,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.sessions.account" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Sessions.Account" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.sessions.account" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$sessions->account" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.AccountAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.account" } }, { @@ -4302,6 +12292,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.getSafeMode" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.GetSafeMode" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.get_safe_mode" + }, + "php": { + "status": "covered", + "method": "Client::$sessions->getSafeMode" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.GetSafeModeAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sessions.get_safe_mode" } }, { @@ -4313,6 +12323,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.retrieveTierChange" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.RetrieveTierChange" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.retrieve_tier_change" + }, + "php": { + "status": "covered", + "method": "Client::sessions.retrieveTierChange" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.RetrieveTierChangeAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sessions.retrieve_tier_change" } }, { @@ -4324,6 +12354,26 @@ "typescript": { "status": "covered", "method": "Client.sipTrunks.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SIPTrunks.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sip_trunks.list" + }, + "php": { + "status": "covered", + "method": "Client::sipTrunks.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SipTrunks.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sip_trunks.list" } }, { @@ -4335,6 +12385,26 @@ "typescript": { "status": "covered", "method": "Client.sipTrunks.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SIPTrunks.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sip_trunks.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::sipTrunks.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SipTrunks.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sip_trunks.retrieve" } }, { @@ -4346,6 +12416,26 @@ "typescript": { "status": "covered", "method": "Client.sipTrunks.endpoint" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SIPTrunks.Endpoint" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sip_trunks.endpoint" + }, + "php": { + "status": "covered", + "method": "Client::sipTrunks.endpoint" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SipTrunks.EndpointAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sip_trunks.endpoint" } }, { @@ -4357,6 +12447,26 @@ "typescript": { "status": "covered", "method": "Client.organizations.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Organizations.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.organizations.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::organizations.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Organizations.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.organizations.retrieve" } }, { @@ -4369,6 +12479,29 @@ "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by Client.", "milestone": "credential-boundary" + }, + "go": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client." + }, + "python": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" + }, + "php": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" + }, + "dotnet": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client." + }, + "rust": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" } }, { @@ -4381,6 +12514,29 @@ "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by Client.", "milestone": "credential-boundary" + }, + "go": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client." + }, + "python": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" + }, + "php": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" + }, + "dotnet": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client." + }, + "rust": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" } }, { @@ -4392,6 +12548,26 @@ "typescript": { "status": "covered", "method": "Client.projectTokens.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.ProjectTokens.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.project_tokens.list" + }, + "php": { + "status": "covered", + "method": "Client::projectTokens.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.ProjectTokens.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.project_tokens.list" } }, { @@ -4403,6 +12579,26 @@ "typescript": { "status": "covered", "method": "Client.usage.summary" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Usage.Summary" + }, + "python": { + "status": "covered", + "method": "AsyncClient.usage.summary" + }, + "php": { + "status": "covered", + "method": "Client::usage.summary" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Usage.SummaryAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.usage.summary" } }, { @@ -4414,6 +12610,26 @@ "typescript": { "status": "covered", "method": "Client.usage.listRecords" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Usage.ListRecords" + }, + "python": { + "status": "covered", + "method": "AsyncClient.usage.list_records" + }, + "php": { + "status": "covered", + "method": "Client::usage.listRecords" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Usage.ListRecordsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.usage.list_records" } }, { @@ -4425,6 +12641,26 @@ "typescript": { "status": "covered", "method": "Client.voice.audio.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.audio.list" + }, + "php": { + "status": "covered", + "method": "Client::voice.audio.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.audio.list" } }, { @@ -4436,17 +12672,57 @@ "typescript": { "status": "covered", "method": "Client.voice.audio.retrieve" - } - }, - { - "family": "platform", - "method": "GET", + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.audio.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::voice.audio.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.audio.retrieve" + } + }, + { + "family": "platform", + "method": "GET", "path": "/platform/voice/audio/{assetId}/preview", "operationId": "getVoiceAudioPreview", "fingerprint": "73eeead51e68c692a503931cb0492a920b9834e1522aa91f2f3b5cac6f3c02f5", "typescript": { "status": "covered", "method": "Client.voice.audio.previewUrl" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.PreviewURL" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.audio.preview_url" + }, + "php": { + "status": "covered", + "method": "Client::voice.audio.previewUrl" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.PreviewUrlAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.audio.preview_url" } }, { @@ -4458,6 +12734,26 @@ "typescript": { "status": "covered", "method": "Client.voice.providerCredentials.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.ProviderCredentials.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.provider_credentials.list" + }, + "php": { + "status": "covered", + "method": "Client::voice.providerCredentials.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.ProviderCredentials.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.provider_credentials" } }, { @@ -4469,6 +12765,26 @@ "typescript": { "status": "covered", "method": "Client.voice.providerCredentials.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.ProviderCredentials.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.provider_credentials.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::voice.providerCredentials.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.ProviderCredentials.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.provider_credentials.retrieve" } }, { @@ -4480,6 +12796,26 @@ "typescript": { "status": "covered", "method": "Client.webhookDeliveries.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.WebhookDeliveries.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhook_deliveries.list" + }, + "php": { + "status": "covered", + "method": "Client::webhookDeliveries.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.WebhookDeliveries.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhook_deliveries.list" } }, { @@ -4491,6 +12827,26 @@ "typescript": { "status": "covered", "method": "Client.webhookDeliveries.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.WebhookDeliveries.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhook_deliveries.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::webhookDeliveries.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.WebhookDeliveries.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhook_deliveries.retrieve" } }, { @@ -4502,6 +12858,26 @@ "typescript": { "status": "covered", "method": "Client.webhookDeliveries.listAttempts" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.WebhookDeliveries.ListAttempts" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhook_deliveries.list_attempts" + }, + "php": { + "status": "covered", + "method": "Client::webhookDeliveries.listAttempts" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.WebhookDeliveries.ListAttemptsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhook_deliveries.list_attempts" } }, { @@ -4513,6 +12889,26 @@ "typescript": { "status": "covered", "method": "Client.webhookDeliveries.retrieveAttempt" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.WebhookDeliveries.RetrieveAttempt" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhook_deliveries.retrieve_attempt" + }, + "php": { + "status": "covered", + "method": "Client::webhookDeliveries.retrieveAttempt" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.WebhookDeliveries.RetrieveAttemptAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhook_deliveries.retrieve_attempt" } }, { @@ -4524,6 +12920,26 @@ "typescript": { "status": "covered", "method": "Client.webhooks.list" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Webhooks.List" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhooks.list" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.list" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Webhooks.ListAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhooks.list" } }, { @@ -4535,6 +12951,26 @@ "typescript": { "status": "covered", "method": "Client.webhooks.retrieve" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Webhooks.Retrieve" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhooks.retrieve" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.retrieve" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Webhooks.RetrieveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhooks.retrieve" } }, { @@ -4546,6 +12982,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4557,6 +13016,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4568,6 +13050,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4579,6 +13084,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4590,6 +13118,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4601,6 +13152,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -4612,6 +13186,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4623,6 +13220,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.update" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Update" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.update" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.update" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.update" } }, { @@ -4634,6 +13251,26 @@ "typescript": { "status": "covered", "method": "Client.customers.update" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.Update" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.update" + }, + "php": { + "status": "covered", + "method": "Client::customers.update" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.update" } }, { @@ -4645,6 +13282,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.update" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Update" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.update" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.update" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.update" } }, { @@ -4656,6 +13313,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.update" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Update" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.update" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.update" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.update" } }, { @@ -4667,6 +13344,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhooks.update" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Webhooks.Update" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhooks.update" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.update" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Webhooks.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhooks.update" } }, { @@ -4678,6 +13375,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.setTierOverride" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.SetTierOverride" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.set_tier_override" + }, + "php": { + "status": "covered", + "method": "Client::sessions.setTierOverride" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.SetTierOverrideAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sessions.set_tier_override" } }, { @@ -4689,6 +13406,26 @@ "typescript": { "status": "covered", "method": "Client.sipTrunks.update" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SIPTrunks.Update" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sip_trunks.update" + }, + "php": { + "status": "covered", + "method": "Client::sipTrunks.update" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SipTrunks.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sip_trunks.update" } }, { @@ -4701,6 +13438,29 @@ "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by Client.", "milestone": "credential-boundary" + }, + "go": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client." + }, + "python": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" + }, + "php": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" + }, + "dotnet": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client." + }, + "rust": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" } }, { @@ -4712,6 +13472,26 @@ "typescript": { "status": "covered", "method": "Client.voice.audio.update" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.Update" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.audio.update" + }, + "php": { + "status": "covered", + "method": "Client::voice.audio.update" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.audio.update" } }, { @@ -4723,6 +13503,26 @@ "typescript": { "status": "covered", "method": "Client.webhooks.update" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Webhooks.Update" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhooks.update" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.update" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Webhooks.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhooks.update" } }, { @@ -4734,6 +13534,29 @@ "typescript": { "status": "excluded", "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "go": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "python": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "rust": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "not-server-sdk" } }, { @@ -4745,6 +13568,29 @@ "typescript": { "status": "excluded", "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "go": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "python": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "rust": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "not-server-sdk" } }, { @@ -4756,6 +13602,29 @@ "typescript": { "status": "excluded", "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "go": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "python": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "rust": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "not-server-sdk" } }, { @@ -4767,6 +13636,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4778,6 +13670,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4789,6 +13704,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4800,6 +13738,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4811,6 +13772,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4822,6 +13806,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4833,6 +13840,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4844,6 +13874,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4855,6 +13908,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4866,6 +13942,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4877,6 +13976,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4888,6 +14010,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4899,6 +14044,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4910,6 +14078,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4921,6 +14112,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4932,6 +14146,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4943,6 +14180,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4954,6 +14214,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4965,6 +14248,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4976,6 +14282,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4987,6 +14316,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -4998,6 +14350,29 @@ "typescript": { "status": "excluded", "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -5009,6 +14384,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -5020,6 +14418,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -5031,6 +14452,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -5042,6 +14486,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -5053,6 +14520,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -5064,6 +14554,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -5075,6 +14588,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -5086,17 +14622,63 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." - } - }, - { - "family": "platform", - "method": "POST", - "path": "/console/voice/audio/{assetId}/complete", - "operationId": "completeConsoleVoiceAudioUpload", - "fingerprint": "f0699a8103e5d08786cb8d5a5110397008a53f9aa7cbcd9b67685b4a2b64f3d6", - "typescript": { + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" + } + }, + { + "family": "platform", + "method": "POST", + "path": "/console/voice/audio/{assetId}/complete", + "operationId": "completeConsoleVoiceAudioUpload", + "fingerprint": "f0699a8103e5d08786cb8d5a5110397008a53f9aa7cbcd9b67685b4a2b64f3d6", + "typescript": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { "status": "excluded", - "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -5108,6 +14690,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -5119,6 +14724,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -5130,6 +14758,29 @@ "typescript": { "status": "excluded", "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "go": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "python": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + "rust": { + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes.", + "milestone": "not-server-sdk" } }, { @@ -5141,6 +14792,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -5152,6 +14826,26 @@ "typescript": { "status": "covered", "method": "Client.audiences.create" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Audiences.Create" + }, + "python": { + "status": "covered", + "method": "AsyncClient.audiences.create" + }, + "php": { + "status": "covered", + "method": "Client::audiences.create" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Audiences.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.audiences.create" } }, { @@ -5163,6 +14857,26 @@ "typescript": { "status": "covered", "method": "Client.audiences.addMembers" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Audiences.AddMembers" + }, + "python": { + "status": "covered", + "method": "AsyncClient.audiences.add_members" + }, + "php": { + "status": "covered", + "method": "Client::audiences.addMembers" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Audiences.AddMembersAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.audiences.add_members" } }, { @@ -5174,6 +14888,26 @@ "typescript": { "status": "covered", "method": "Client.audiences.createUpload" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Audiences.CreateUpload" + }, + "python": { + "status": "covered", + "method": "AsyncClient.audiences.create_upload" + }, + "php": { + "status": "covered", + "method": "Client::audiences.createUpload" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Audiences.CreateUploadAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.audiences.create_upload" } }, { @@ -5185,6 +14919,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.createIncident" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.CreateIncident" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.create_incident" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.createIncident" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.CreateIncidentAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.create_incident" } }, { @@ -5196,6 +14950,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.retractIncident" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.BanSafe.RetractIncident" + }, + "python": { + "status": "covered", + "method": "AsyncClient.ban_safe.retract_incident" + }, + "php": { + "status": "covered", + "method": "Client::banSafe.retractIncident" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.BanSafe.RetractIncidentAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.ban_safe.retract_incident" } }, { @@ -5207,6 +14981,26 @@ "typescript": { "status": "covered", "method": "Client.callOptOuts.create" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.CallOptOuts.Create" + }, + "python": { + "status": "covered", + "method": "AsyncClient.call_opt_outs.create" + }, + "php": { + "status": "covered", + "method": "Client::callOptOuts.create" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.CallOptOuts.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.call_opt_outs.create" } }, { @@ -5218,6 +15012,26 @@ "typescript": { "status": "covered", "method": "Client.callOptOuts.import" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.CallOptOuts.Import" + }, + "python": { + "status": "covered", + "method": "AsyncClient.call_opt_outs.import_entries" + }, + "php": { + "status": "covered", + "method": "Client::callOptOuts.import" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.CallOptOuts.ImportAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.call_opt_outs.import" } }, { @@ -5229,6 +15043,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.create" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Create" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.create" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.create" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.create" } }, { @@ -5240,6 +15074,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.archive" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Archive" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.archive" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.archive" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.ArchiveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.archive" } }, { @@ -5251,6 +15105,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.recordConversion" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.RecordConversion" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.record_conversion" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.recordConversion" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.RecordConversionAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.record_conversion" } }, { @@ -5262,6 +15136,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.duplicate" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Duplicate" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.duplicate" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.duplicate" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.DuplicateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.duplicate" } }, { @@ -5273,6 +15167,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.launch" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Launch" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.launch" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.launch" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.LaunchAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.launch" } }, { @@ -5284,6 +15198,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.pause" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Pause" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.pause" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.pause" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.PauseAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.pause" } }, { @@ -5295,6 +15229,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.addRecipients" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.AddRecipients" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.add_recipients" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.addRecipients" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.AddRecipientsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.add_recipients" } }, { @@ -5306,6 +15260,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.requeue" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Requeue" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.requeue" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.requeue" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.RequeueAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.requeue" } }, { @@ -5315,8 +15289,28 @@ "operationId": "reschedulePlatformCampaign", "fingerprint": "21878cc0e7fed1e5f4c0f983a4899a70dd5fa4a410b1d293962a8ddccabe2f23", "typescript": { - "status": "excluded", - "reason": "Campaign reschedule is owned by SDK PR #310 and will be covered when that PR merges." + "status": "covered", + "method": "Client.campaigns.reschedule" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Reschedule" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.reschedule" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.reschedule" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.RescheduleAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.reschedule" } }, { @@ -5328,6 +15322,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.resume" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Resume" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.resume" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.resume" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.ResumeAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.resume" } }, { @@ -5339,6 +15353,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.stop" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Campaigns.Stop" + }, + "python": { + "status": "covered", + "method": "AsyncClient.campaigns.stop" + }, + "php": { + "status": "covered", + "method": "Client::campaigns.stop" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Campaigns.StopAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.campaigns.stop" } }, { @@ -5349,7 +15383,27 @@ "fingerprint": "cf0592b5de6bf09edb5ee822d14884e8ac1f1aea75a8ea7674c0aa80ebf9e70c", "typescript": { "status": "covered", - "method": "MessagingClient.clientTokens.mint" + "method": "MessagingClient.clientTokens.mint" + }, + "go": { + "status": "covered", + "method": "MessagingClient.ClientTokens.Mint" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.client_tokens.mint" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$clientTokens->mint" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.ClientTokens.MintAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.client_tokens.mint" } }, { @@ -5361,6 +15415,26 @@ "typescript": { "status": "covered", "method": "Client.customers.create" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.Create" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.create" + }, + "php": { + "status": "covered", + "method": "Client::customers.create" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.create" } }, { @@ -5372,6 +15446,26 @@ "typescript": { "status": "covered", "method": "Client.customers.archive" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.Archive" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.archive" + }, + "php": { + "status": "covered", + "method": "Client::customers.archive" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.ArchiveAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.archive" } }, { @@ -5383,6 +15477,26 @@ "typescript": { "status": "covered", "method": "Client.customers.transferNumber" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.TransferNumber" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.transfer_number" + }, + "php": { + "status": "covered", + "method": "Client::customers.transferNumber" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.TransferNumberAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.transfer_number" } }, { @@ -5394,6 +15508,26 @@ "typescript": { "status": "covered", "method": "Client.customers.createPairingLink" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.CreatePairingLink" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.create_pairing_link" + }, + "php": { + "status": "covered", + "method": "Client::customers.createPairingLink" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.CreatePairingLinkAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.create_pairing_link" } }, { @@ -5405,6 +15539,26 @@ "typescript": { "status": "covered", "method": "Client.customers.restore" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.Restore" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.restore" + }, + "php": { + "status": "covered", + "method": "Client::customers.restore" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.RestoreAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.restore" } }, { @@ -5416,6 +15570,26 @@ "typescript": { "status": "covered", "method": "Client.events.replay" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Events.Replay" + }, + "python": { + "status": "covered", + "method": "AsyncClient.events.replay" + }, + "php": { + "status": "covered", + "method": "Client::events.replay" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Events.ReplayAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.events.replay" } }, { @@ -5427,6 +15601,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.rotateEncryptionKey" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.RotateEncryptionKey" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.rotate_encryption_key" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.rotateEncryptionKey" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.RotateEncryptionKeyAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.rotate_encryption_key" } }, { @@ -5438,6 +15632,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.create" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Create" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.create" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.create" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.create" } }, { @@ -5449,6 +15663,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.deprecate" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Deprecate" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.deprecate" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.deprecate" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.DeprecateAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.deprecate" } }, { @@ -5460,6 +15694,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.discard" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Discard" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.discard" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.discard" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.DiscardAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.discard" } }, { @@ -5471,6 +15725,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.publish" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Publish" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.publish" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.publish" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.PublishAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.publish" } }, { @@ -5482,6 +15756,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.sync" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Sync" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.sync" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.sync" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.SyncAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.sync" } }, { @@ -5493,6 +15787,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.upload" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.Upload" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.upload" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.upload" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.UploadAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.upload" } }, { @@ -5504,6 +15818,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.create" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Create" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.create" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.create" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.create" } }, { @@ -5515,6 +15849,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.deployments.create" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Deployments.Create" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.deployments.create" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.deployments.create" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.Deployments.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.deployments.create" } }, { @@ -5526,6 +15880,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.invocations.create" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Invocations.Create" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.invocations.create" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.invocations.create" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.Invocations.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.invocations.create" } }, { @@ -5537,6 +15911,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.secrets.create" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Secrets.Create" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.secrets.create" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.secrets.create" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.Secrets.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.secrets.create" } }, { @@ -5548,6 +15942,26 @@ "typescript": { "status": "covered", "method": "Client.securityIncidents.acknowledge" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SecurityIncidents.Acknowledge" + }, + "python": { + "status": "covered", + "method": "AsyncClient.security_incidents.acknowledge" + }, + "php": { + "status": "covered", + "method": "Client::securityIncidents.acknowledge" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SecurityIncidents.AcknowledgeAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.security_incidents.acknowledge" } }, { @@ -5559,6 +15973,26 @@ "typescript": { "status": "covered", "method": "Client.media.createUpload" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Media.CreateUpload" + }, + "python": { + "status": "covered", + "method": "AsyncClient.media.create_upload" + }, + "php": { + "status": "covered", + "method": "Client::media.createUpload" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Media.CreateUploadAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.media.create_upload" } }, { @@ -5570,6 +16004,26 @@ "typescript": { "status": "covered", "method": "Client.operations.cancel" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Operations.Cancel" + }, + "python": { + "status": "covered", + "method": "AsyncClient.operations.cancel" + }, + "php": { + "status": "covered", + "method": "Client::operations.cancel" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Operations.CancelAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.operations.cancel" } }, { @@ -5581,6 +16035,26 @@ "typescript": { "status": "covered", "method": "Client.optOuts.create" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.OptOuts.Create" + }, + "python": { + "status": "covered", + "method": "AsyncClient.opt_outs.create" + }, + "php": { + "status": "covered", + "method": "Client::optOuts.create" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.OptOuts.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.opt_outs.create" } }, { @@ -5592,6 +16066,26 @@ "typescript": { "status": "covered", "method": "Client.optOuts.createBatch" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.OptOuts.CreateBatch" + }, + "python": { + "status": "covered", + "method": "AsyncClient.opt_outs.create_batch" + }, + "php": { + "status": "covered", + "method": "Client::optOuts.createBatch" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.OptOuts.CreateBatchAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.opt_outs.create_batch" } }, { @@ -5603,6 +16097,26 @@ "typescript": { "status": "covered", "method": "Client.projects.create" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.Create" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.create" + }, + "php": { + "status": "covered", + "method": "Client::projects.create" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.create" } }, { @@ -5614,6 +16128,26 @@ "typescript": { "status": "covered", "method": "Client.customers.enable" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Customers.Enable" + }, + "python": { + "status": "covered", + "method": "AsyncClient.customers.enable" + }, + "php": { + "status": "covered", + "method": "Client::customers.enable" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Customers.EnableAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.customers.enable" } }, { @@ -5625,6 +16159,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).events.replay" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Events.Replay" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.events.replay" + }, + "php": { + "status": "covered", + "method": "Client::events.replay" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Events.ReplayAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.events.replay" } }, { @@ -5635,7 +16189,27 @@ "fingerprint": "1738b5cb4d9927663ec3c898cdbf5b37ab4b9286a997bd10878745e56248f10d", "typescript": { "status": "covered", - "method": "Client.events.acknowledgeStream" + "method": "Client.events.acknowledgeStream" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Events.AcknowledgeStream" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.events.acknowledge_stream" + }, + "php": { + "status": "covered", + "method": "Client::events.acknowledgeStream" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Events.AcknowledgeStreamAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.events.acknowledge_stream" } }, { @@ -5647,6 +16221,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).operations.cancel" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Operations.Cancel" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.operations.cancel" + }, + "php": { + "status": "covered", + "method": "Client::operations.cancel" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Operations.CancelAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.operations.cancel" } }, { @@ -5658,6 +16252,26 @@ "typescript": { "status": "covered", "method": "Client.projects.approveProductionEnrollment" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.ApproveProductionEnrollment" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.approve_production_enrollment" + }, + "php": { + "status": "covered", + "method": "Client::projects.approveProductionEnrollment" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.ApproveProductionEnrollmentAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.approve_production_enrollment" } }, { @@ -5669,6 +16283,26 @@ "typescript": { "status": "covered", "method": "Client.projects.cancelProductionEnrollment" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.CancelProductionEnrollment" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.cancel_production_enrollment" + }, + "php": { + "status": "covered", + "method": "Client::projects.cancelProductionEnrollment" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.CancelProductionEnrollmentAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.cancel_production_enrollment" } }, { @@ -5680,6 +16314,26 @@ "typescript": { "status": "covered", "method": "Client.projects.requestProductionEnrollment" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.RequestProductionEnrollment" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.request_production_enrollment" + }, + "php": { + "status": "covered", + "method": "Client::projects.requestProductionEnrollment" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.RequestProductionEnrollmentAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.request_production_enrollment" } }, { @@ -5691,6 +16345,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhookDeliveries.retry" + }, + "go": { + "status": "covered", + "method": "ProjectClient.WebhookDeliveries.Retry" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhook_deliveries.retry" + }, + "php": { + "status": "covered", + "method": "Client::webhookDeliveries.retry" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.WebhookDeliveries.RetryAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhook_deliveries.retry" } }, { @@ -5702,6 +16376,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhooks.create" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Webhooks.Create" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhooks.create" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.create" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Webhooks.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhooks.create" } }, { @@ -5713,6 +16407,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhooks.rotateSecret" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Webhooks.RotateSecret" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhooks.rotate_secret" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.rotateSecret" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Webhooks.RotateSecretAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhooks.rotate_secret" } }, { @@ -5724,6 +16438,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhooks.test" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Webhooks.Test" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.webhooks.test" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.test" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Webhooks.TestAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.webhooks.test" } }, { @@ -5736,6 +16470,29 @@ "status": "excluded", "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", "milestone": "security-design" + }, + "go": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + "python": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" + }, + "php": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" + }, + "dotnet": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + "rust": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" } }, { @@ -5748,6 +16505,29 @@ "status": "excluded", "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", "milestone": "security-design" + }, + "go": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + "python": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" + }, + "php": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" + }, + "dotnet": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + "rust": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" } }, { @@ -5760,6 +16540,29 @@ "status": "excluded", "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", "milestone": "security-design" + }, + "go": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + "python": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" + }, + "php": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" + }, + "dotnet": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + "rust": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" } }, { @@ -5771,6 +16574,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.sessions.logout" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Sessions.Logout" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.sessions.logout" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$sessions->logout" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.LogoutAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.logout" } }, { @@ -5782,6 +16605,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.sessions.restart" + }, + "go": { + "status": "covered", + "method": "MessagingClient.Sessions.Restart" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.sessions.restart" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$sessions->restart" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.RestartAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.restart" } }, { @@ -5793,6 +16636,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.start" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.Start" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.start" + }, + "php": { + "status": "covered", + "method": "Client::sessions.start" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.StartAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.start" } }, { @@ -5804,6 +16667,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.stop" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.Stop" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.stop" + }, + "php": { + "status": "covered", + "method": "Client::sessions.stop" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.StopAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.stop" } }, { @@ -5815,6 +16698,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.quoteTierChange" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.QuoteTierChange" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.quote_tier_change" + }, + "php": { + "status": "covered", + "method": "Client::sessions.quoteTierChange" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.QuoteTierChangeAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sessions.quote_tier_change" } }, { @@ -5827,6 +16730,29 @@ "status": "excluded", "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", "milestone": "security-design" + }, + "go": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + "python": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" + }, + "php": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" + }, + "dotnet": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + "rust": { + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete.", + "milestone": "security-design" } }, { @@ -5838,6 +16764,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.deleteMany" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.DeleteMany" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.delete_many" + }, + "php": { + "status": "covered", + "method": "Client::sessions.deleteMany" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.DeleteManyAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sessions.delete_many" } }, { @@ -5849,6 +16795,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.stopMany" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.StopMany" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.stop_many" + }, + "php": { + "status": "covered", + "method": "Client::sessions.stopMany" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.StopManyAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sessions.stop_many" } }, { @@ -5860,6 +16826,26 @@ "typescript": { "status": "covered", "method": "Client.sipTrunks.create" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SIPTrunks.Create" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sip_trunks.create" + }, + "php": { + "status": "covered", + "method": "Client::sipTrunks.create" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SipTrunks.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sip_trunks.create" } }, { @@ -5871,6 +16857,26 @@ "typescript": { "status": "covered", "method": "Client.sipTrunks.rotateCredentials" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SIPTrunks.RotateCredentials" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sip_trunks.rotate_credentials" + }, + "php": { + "status": "covered", + "method": "Client::sipTrunks.rotateCredentials" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.SipTrunks.RotateCredentialsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sip_trunks.rotate_credentials" } }, { @@ -5883,6 +16889,29 @@ "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by Client.", "milestone": "credential-boundary" + }, + "go": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client." + }, + "python": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" + }, + "php": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" + }, + "dotnet": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client." + }, + "rust": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by Client.", + "milestone": "credential-boundary" } }, { @@ -5894,6 +16923,26 @@ "typescript": { "status": "covered", "method": "Client.voice.audio.createUpload" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.CreateUpload" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.audio.create_upload" + }, + "php": { + "status": "covered", + "method": "Client::voice.audio.createUpload" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.CreateUploadAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.audio.create_upload" } }, { @@ -5905,6 +16954,26 @@ "typescript": { "status": "covered", "method": "Client.voice.audio.complete" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.Complete" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.audio.complete" + }, + "php": { + "status": "covered", + "method": "Client::voice.audio.complete" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.CompleteAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.audio.complete" } }, { @@ -5916,6 +16985,26 @@ "typescript": { "status": "covered", "method": "Client.voice.audio.synthesize" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.Synthesize" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.audio.synthesize" + }, + "php": { + "status": "covered", + "method": "Client::voice.audio.synthesize" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.Audio.SynthesizeAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.audio.synthesize" } }, { @@ -5927,6 +17016,26 @@ "typescript": { "status": "covered", "method": "Client.voice.providerCredentials.create" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.ProviderCredentials.Create" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.provider_credentials.create" + }, + "php": { + "status": "covered", + "method": "Client::voice.providerCredentials.create" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.ProviderCredentials.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.provider_credentials.create" } }, { @@ -5938,6 +17047,26 @@ "typescript": { "status": "covered", "method": "Client.voice.providerCredentials.verify" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Voice.ProviderCredentials.Verify" + }, + "python": { + "status": "covered", + "method": "AsyncClient.voice.provider_credentials.verify" + }, + "php": { + "status": "covered", + "method": "Client::voice.providerCredentials.verify" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Voice.ProviderCredentials.VerifyAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.voice.provider_credentials.verify" } }, { @@ -5949,6 +17078,26 @@ "typescript": { "status": "covered", "method": "Client.webhookDeliveries.retry" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.WebhookDeliveries.Retry" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhook_deliveries.retry" + }, + "php": { + "status": "covered", + "method": "Client::webhookDeliveries.retry" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.WebhookDeliveries.RetryAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhook_deliveries.retry" } }, { @@ -5960,6 +17109,26 @@ "typescript": { "status": "covered", "method": "Client.webhooks.create" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Webhooks.Create" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhooks.create" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.create" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Webhooks.CreateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhooks.create" } }, { @@ -5971,6 +17140,26 @@ "typescript": { "status": "covered", "method": "Client.webhooks.rotateSecret" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Webhooks.RotateSecret" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhooks.rotate_secret" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.rotateSecret" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Webhooks.RotateSecretAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhooks.rotate_secret" } }, { @@ -5982,6 +17171,26 @@ "typescript": { "status": "covered", "method": "Client.webhooks.test" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Webhooks.Test" + }, + "python": { + "status": "covered", + "method": "AsyncClient.webhooks.test" + }, + "php": { + "status": "covered", + "method": "Client::webhooks.test" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Webhooks.TestAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.webhooks.test" } }, { @@ -5993,6 +17202,29 @@ "typescript": { "status": "excluded", "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "go": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "python": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + "rust": { + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK.", + "milestone": "not-server-sdk" } }, { @@ -6004,6 +17236,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -6015,6 +17270,29 @@ "typescript": { "status": "excluded", "reason": "Console-only dashboard operation; not a public SDK method." + }, + "go": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method." + }, + "python": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method." + }, + "rust": { + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method.", + "milestone": "not-server-sdk" } }, { @@ -6026,6 +17304,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -6037,6 +17338,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -6048,6 +17372,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -6059,6 +17406,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -6070,6 +17440,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -6081,6 +17474,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "not-server-sdk" } }, { @@ -6092,6 +17508,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; organization API keys and project tokens are not accepted.", + "milestone": "not-server-sdk" } }, { @@ -6103,6 +17542,29 @@ "typescript": { "status": "excluded", "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -6114,6 +17576,29 @@ "typescript": { "status": "excluded", "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "go": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "python": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + "rust": { + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials.", + "milestone": "not-server-sdk" } }, { @@ -6125,6 +17610,29 @@ "typescript": { "status": "excluded", "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "go": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "python": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + "rust": { + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface.", + "milestone": "not-server-sdk" } }, { @@ -6136,6 +17644,26 @@ "typescript": { "status": "covered", "method": "Client.billing.setResourceControls" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.SetResourceControls" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.set_resource_controls" + }, + "php": { + "status": "covered", + "method": "Client::billing.setResourceControls" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.SetResourceControlsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.set_resource_controls" } }, { @@ -6147,6 +17675,26 @@ "typescript": { "status": "covered", "method": "Client.billing.setLimit" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.SetLimit" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.set_limit" + }, + "php": { + "status": "covered", + "method": "Client::billing.setLimit" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.SetLimitAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.set_limit" } }, { @@ -6158,6 +17706,26 @@ "typescript": { "status": "covered", "method": "Client.billing.reorderPriorities" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.ReorderPriorities" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.reorder_priorities" + }, + "php": { + "status": "covered", + "method": "Client::billing.reorderPriorities" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.ReorderPrioritiesAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.reorder_priorities" } }, { @@ -6169,6 +17737,26 @@ "typescript": { "status": "covered", "method": "Client.billing.setPriority" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Billing.SetPriority" + }, + "python": { + "status": "covered", + "method": "AsyncClient.billing.set_priority" + }, + "php": { + "status": "covered", + "method": "Client::billing.setPriority" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Billing.SetPriorityAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.billing.set_priority" } }, { @@ -6180,6 +17768,26 @@ "typescript": { "status": "covered", "method": "Client.callPolicy.update" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.CallPolicy.Update" + }, + "python": { + "status": "covered", + "method": "AsyncClient.call_policy.update" + }, + "php": { + "status": "covered", + "method": "Client::callPolicy.update" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.CallPolicy.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.call_policy.update" } }, { @@ -6191,6 +17799,26 @@ "typescript": { "status": "covered", "method": "Client.callRetention.update" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.CallRetention.Update" + }, + "python": { + "status": "covered", + "method": "AsyncClient.call_retention.update" + }, + "php": { + "status": "covered", + "method": "Client::callRetention.update" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.CallRetention.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.call_retention.update" } }, { @@ -6202,6 +17830,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).flows.setEndpoint" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Flows.SetEndpoint" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.flows.set_endpoint" + }, + "php": { + "status": "covered", + "method": "ProjectClient::flows.setEndpoint" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Flows.SetEndpointAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.flows.set_endpoint" } }, { @@ -6213,6 +17861,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).functions.deployments.promote" + }, + "go": { + "status": "covered", + "method": "ProjectClient.Functions.Deployments.Promote" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.functions.deployments.promote" + }, + "php": { + "status": "covered", + "method": "ProjectClient::functions.deployments.promote" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Functions.Deployments.PromoteAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.functions.deployments.promote" } }, { @@ -6224,6 +17892,26 @@ "typescript": { "status": "covered", "method": "Client.optOuts.updateSettings" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.OptOuts.UpdateSettings" + }, + "python": { + "status": "covered", + "method": "AsyncClient.opt_outs.update_settings" + }, + "php": { + "status": "covered", + "method": "Client::optOuts.updateSettings" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.OptOuts.UpdateSettingsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.opt_outs.update_settings" } }, { @@ -6235,6 +17923,26 @@ "typescript": { "status": "covered", "method": "Client.projects.updateHealthPolicy" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.UpdateHealthPolicy" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.update_health_policy" + }, + "php": { + "status": "covered", + "method": "Client::$projects->updateHealthPolicy" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.UpdateHealthPolicyAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.update_health_policy" } }, { @@ -6246,6 +17954,26 @@ "typescript": { "status": "covered", "method": "Client.projects.updateInsuranceEvidence" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.UpdateInsuranceEvidence" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.update_insurance_evidence" + }, + "php": { + "status": "covered", + "method": "Client::$projects->updateInsuranceEvidence" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.UpdateInsuranceEvidenceAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.update_insurance_evidence" } }, { @@ -6257,6 +17985,26 @@ "typescript": { "status": "covered", "method": "Client.projects.updateSafeMode" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.UpdateSafeMode" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.update_safe_mode" + }, + "php": { + "status": "covered", + "method": "Client::$projects->updateSafeMode" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.UpdateSafeModeAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.update_safe_mode" } }, { @@ -6268,6 +18016,26 @@ "typescript": { "status": "covered", "method": "Client.projects.updateWarmupPlan" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Projects.UpdateWarmupPlan" + }, + "python": { + "status": "covered", + "method": "AsyncClient.projects.update_warmup_plan" + }, + "php": { + "status": "covered", + "method": "Client::$projects->updateWarmupPlan" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Projects.UpdateWarmupPlanAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.projects.update_warmup_plan" } }, { @@ -6279,6 +18047,26 @@ "typescript": { "status": "covered", "method": "Client.quickLinkSettings.update" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.QuickLinkSettings.Update" + }, + "python": { + "status": "covered", + "method": "AsyncClient.quick_link_settings.update" + }, + "php": { + "status": "covered", + "method": "Client::quickLinkSettings.update" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.QuickLinkSettings.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.quick_link_settings.update" } }, { @@ -6290,6 +18078,26 @@ "typescript": { "status": "covered", "method": "Client.sessionConfiguration.update" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.SessionConfiguration.Update" + }, + "python": { + "status": "covered", + "method": "AsyncClient.session_configuration.update" + }, + "php": { + "status": "covered", + "method": "Client::sessionConfiguration.update" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Configuration.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.session_configuration.update" } }, { @@ -6301,6 +18109,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.voip.updateCallSettings" + }, + "go": { + "status": "covered", + "method": "MessagingClient.VoIP.UpdateCallSettings" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.voip.update_call_settings" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$voip->updateCallSettings" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Calls.UpdateCallSettingsAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.voip.update_call_settings" } }, { @@ -6312,6 +18140,26 @@ "typescript": { "status": "covered", "method": "MessagingClient.clientTokens.updateRules" + }, + "go": { + "status": "covered", + "method": "MessagingClient.ClientTokens.UpdateRules" + }, + "python": { + "status": "covered", + "method": "AsyncMessagingClient.client_tokens.update_rules" + }, + "php": { + "status": "covered", + "method": "MessagingClient::$clientTokens->updateRules" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.ClientTokens.UpdateRulesAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.client_tokens.update_rules" } }, { @@ -6323,6 +18171,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.update" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.Update" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.update" + }, + "php": { + "status": "covered", + "method": "Client::sessions.update" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.UpdateAsync" + }, + "rust": { + "status": "covered", + "method": "MessagingClient.sessions.update" } }, { @@ -6334,6 +18202,26 @@ "typescript": { "status": "covered", "method": "Client.sessions.updateSafeMode" + }, + "go": { + "status": "covered", + "method": "OrganizationClient.Sessions.UpdateSafeMode" + }, + "python": { + "status": "covered", + "method": "AsyncClient.sessions.update_safe_mode" + }, + "php": { + "status": "covered", + "method": "Client::$sessions->updateSafeMode" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Sessions.UpdateSafeModeAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.sessions.update_safe_mode" } }, { @@ -6348,7 +18236,27 @@ }, "sourceContractSha256": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8", "sourceCommit": "171a9683ff45d11a4b5d3917dc428ad2722b0d99", - "sdkContractSha256": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8" + "sdkContractSha256": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8", + "go": { + "status": "covered", + "method": "OrganizationClient.Analytics.Get" + }, + "python": { + "status": "covered", + "method": "AsyncClient.analytics.get" + }, + "php": { + "status": "covered", + "method": "Client::analytics.get" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Analytics.GetAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.analytics.get" + } }, { "family": "platform", @@ -6362,7 +18270,27 @@ }, "sourceContractSha256": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8", "sourceCommit": "171a9683ff45d11a4b5d3917dc428ad2722b0d99", - "sdkContractSha256": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8" + "sdkContractSha256": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8", + "go": { + "status": "covered", + "method": "ProjectClient.Analytics.Get" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.analytics.get" + }, + "php": { + "status": "covered", + "method": "ProjectClient::analytics.get" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Analytics.GetAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.analytics.get" + } }, { "family": "platform", @@ -6376,7 +18304,27 @@ "fingerprint": "520ce37bc6a198e313d9495ddd968ebb54cbbe5cc84f4d6bea636c43a4ea62c5", "sourceContractSha256": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8", "sdkContractSha256": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8", - "sourceCommit": "171a9683ff45d11a4b5d3917dc428ad2722b0d99" + "sourceCommit": "171a9683ff45d11a4b5d3917dc428ad2722b0d99", + "go": { + "status": "covered", + "method": "OrganizationClient.Analytics.Metrics" + }, + "python": { + "status": "covered", + "method": "AsyncClient.analytics.metrics" + }, + "php": { + "status": "covered", + "method": "Client::analytics.metrics" + }, + "dotnet": { + "status": "covered", + "method": "OrganizationClient.Analytics.MetricsAsync" + }, + "rust": { + "status": "covered", + "method": "OrganizationClient.analytics.metrics" + } }, { "family": "platform", @@ -6390,7 +18338,27 @@ "fingerprint": "56e2540bc5d78c3f458fbd363d63b101df49fcd273b7cc81e1fa9318d69b30b6", "sourceContractSha256": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8", "sdkContractSha256": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8", - "sourceCommit": "171a9683ff45d11a4b5d3917dc428ad2722b0d99" + "sourceCommit": "171a9683ff45d11a4b5d3917dc428ad2722b0d99", + "go": { + "status": "covered", + "method": "ProjectClient.Analytics.Metrics" + }, + "python": { + "status": "covered", + "method": "AsyncProjectClient.analytics.metrics" + }, + "php": { + "status": "covered", + "method": "ProjectClient::analytics.metrics" + }, + "dotnet": { + "status": "covered", + "method": "ProjectClient.Analytics.MetricsAsync" + }, + "rust": { + "status": "covered", + "method": "ProjectClient.analytics.metrics" + } }, { "family": "platform", @@ -6401,7 +18369,43 @@ "typescript": { "status": "excluded", "reason": "Dashboard-only operational error vitals require a live Console identity; server API keys do not grant this authority." + }, + "go": { + "status": "excluded", + "reason": "Dashboard-only operational error vitals require a live Console identity; server API keys do not grant this authority." + }, + "python": { + "status": "excluded", + "reason": "Dashboard-only operational error vitals require a live Console identity; server API keys do not grant this authority.", + "milestone": "excluded-contract" + }, + "php": { + "status": "excluded", + "reason": "Dashboard-only operational error vitals require a live Console identity; server API keys do not grant this authority.", + "milestone": "excluded-contract" + }, + "dotnet": { + "status": "excluded", + "reason": "Dashboard-only operational error vitals require a live Console identity; server API keys do not grant this authority." + }, + "rust": { + "status": "excluded", + "reason": "Dashboard-only operational error vitals require a live Console identity; server API keys do not grant this authority.", + "milestone": "not-server-sdk" } } - ] + ], + "languages": ["typescript", "go", "python", "php", "dotnet", "rust"], + "supplements": { + "officialApiRound2": { + "inventory": "contracts/native-official-round2.json", + "baselineUnchanged": true, + "published": false + }, + "simultaneousCalls": { + "inventory": "contracts/native-calls-simultaneous-calls.json", + "baselineUnchanged": true, + "published": false + } + } } diff --git a/contracts/doc-snippets.json b/contracts/doc-snippets.json new file mode 100644 index 00000000..d2ea3794 --- /dev/null +++ b/contracts/doc-snippets.json @@ -0,0 +1,813 @@ +{ + "schemaVersion": 1, + "sourceCommit": "ef1b2eb1b8bb64ce40dbb115d9f1cbb180ad3515", + "sourceFilesSha256": "77ac5e4f0867d06ee5f0e2b06b79aebdd533d2c6a0d8219222f3225c068bab53", + "apiVersion": "2026-09-22", + "apiSourceCommit": "171a9683ff45d11a4b5d3917dc428ad2722b0d99", + "specs": { + "messaging": "7f6ddab25a673e45da7a209292ddfd7354acccf30c27e2fa651113e321b66d3d", + "platform": "71454ab4bb66d844aeedd325d9a62086cd6b6ba39a135197fd4f5ca29ef089e8" + }, + "releaseStatus": "source-only", + "packages": { + "typescript": { "name": "@polymorfa/sdk", "version": "0.1.0-dev.0" }, + "go": { + "name": "github.com/polymorfa/sdks/packages/go", + "version": "0.1.0-dev.0" + }, + "python": { "name": "polymorfa-sdk", "version": "0.1.0.dev0" }, + "php": { "name": "polymorfa/sdk", "version": "0.1.0-dev.0" }, + "dotnet": { "name": "Polymorfa.Sdk", "version": "0.1.0-dev.0" }, + "rust": { "name": "polymorfa-sdk", "version": "0.1.0-dev.0" } + }, + "operations": { + "createBusinessCatalog": { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/business/catalog", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.createCatalog(\"{{session}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().CreateCatalog(ctx, \"{{session}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.create_catalog(\"{{session}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->createCatalog(\"{{session}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.CreateCatalogAsync(\"{{session}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().create_catalog(\"{{session}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "createBusinessCollection": { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/business/collections", + "credentialKind": "organization_api_key", + "request": { + "body": { "name": "Featured", "productIds": ["product-id"] } + }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.createCollection(\"{{session}}\", {\"name\": \"Featured\", \"productIds\": [\"product-id\"]});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().CreateCollection(ctx, \"{{session}}\", sdk.BusinessCollectionCreateRequest{Name: \"Featured\", ProductIDs: []string{\"product-id\"}})\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.create_collection(\"{{session}}\", {\"name\": \"Featured\", \"productIds\": [\"product-id\"]})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->createCollection(\"{{session}}\", [\"name\" => \"Featured\", \"productIds\" => [\"product-id\"]]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.CreateCollectionAsync(\"{{session}}\", new BusinessCollectionCreateRequest(\"Featured\", new[] { \"product-id\" }));\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().create_collection(\"{{session}}\", &polymorfa_sdk::business::BusinessCollectionCreateRequest { name: \"Featured\".into(), product_ids: vec![\"product-id\".into()] }, Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "deleteBusinessCollection": { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/business/collections/{collectionId}", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.deleteCollection(\"{{session}}\", \"{{collectionId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().DeleteCollection(ctx, \"{{session}}\", \"{{collectionId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.delete_collection(\"{{session}}\", \"{{collectionId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->deleteCollection(\"{{session}}\", \"{{collectionId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.DeleteCollectionAsync(\"{{session}}\", \"{{collectionId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().delete_collection(\"{{session}}\", \"{{collectionId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "deleteBusinessCoverPhoto": { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/business/profile/cover-photo/{coverPhotoId}", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.deleteCoverPhoto(\"{{session}}\", \"{{coverPhotoId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().DeleteCoverPhoto(ctx, \"{{session}}\", \"{{coverPhotoId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.delete_cover_photo(\"{{session}}\", \"{{coverPhotoId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->deleteCoverPhoto(\"{{session}}\", \"{{coverPhotoId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.DeleteCoverPhotoAsync(\"{{session}}\", \"{{coverPhotoId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().delete_cover_photo(\"{{session}}\", \"{{coverPhotoId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "deleteBusinessProduct": { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/business/products/{productId}", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.deleteProduct(\"{{session}}\", \"{{productId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().DeleteProduct(ctx, \"{{session}}\", \"{{productId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.delete_product(\"{{session}}\", \"{{productId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->deleteProduct(\"{{session}}\", \"{{productId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.DeleteProductAsync(\"{{session}}\", \"{{productId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().delete_product(\"{{session}}\", \"{{productId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "deletePlatformSession": { + "family": "platform", + "method": "DELETE", + "path": "/platform/sessions/{sessionId}", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.sessions.delete(\"{{sessionId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Sessions().Delete(ctx, \"{{sessionId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.sessions.delete(\"{{sessionId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "sessions->delete(\"{{sessionId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Sessions.DeleteAsync(\"{{sessionId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.sessions().delete(\"{{sessionId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getBusinessCatalog": { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/catalog", + "credentialKind": "organization_api_key", + "request": { "query": { "id": "42" } }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.getCatalog(\"{{session}}\", {\"id\": \"42\"});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().GetCatalog(ctx, \"{{session}}\", sdk.BusinessCatalogParams{ID: \"42\"})\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.get_catalog(\"{{session}}\", {\"id\": \"42\"})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->getCatalog(\"{{session}}\", [\"id\" => \"42\"]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.GetCatalogAsync(\"{{session}}\", new BusinessCatalogParameters(\"42\"));\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().get_catalog(\"{{session}}\", &polymorfa_sdk::business::BusinessCatalogParameters { id: \"42\".into(), after: None, limit: None, width: None, height: None }, Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getBusinessCollection": { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/collections/{collectionId}", + "credentialKind": "organization_api_key", + "request": { "query": { "id": "42" } }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.getCollection(\"{{session}}\", \"{{collectionId}}\", {\"id\": \"42\"});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().GetCollection(ctx, \"{{session}}\", \"{{collectionId}}\", sdk.BusinessCollectionParams{ID: \"42\"})\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.get_collection(\"{{session}}\", \"{{collectionId}}\", {\"id\": \"42\"})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->getCollection(\"{{session}}\", \"{{collectionId}}\", [\"id\" => \"42\"]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.GetCollectionAsync(\"{{session}}\", \"{{collectionId}}\", new BusinessCatalogParameters(\"42\"));\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().get_collection(\"{{session}}\", \"{{collectionId}}\", &polymorfa_sdk::business::BusinessCatalogParameters { id: \"42\".into(), after: None, limit: None, width: None, height: None }, Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getBusinessCollections": { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/collections", + "credentialKind": "organization_api_key", + "request": { "query": { "id": "42" } }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.listCollections(\"{{session}}\", {\"id\": \"42\"});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().ListCollections(ctx, \"{{session}}\", sdk.BusinessCollectionsParams{ID: \"42\"})\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.list_collections(\"{{session}}\", {\"id\": \"42\"})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->listCollections(\"{{session}}\", [\"id\" => \"42\"]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.ListCollectionsAsync(\"{{session}}\", new BusinessCollectionsParameters(\"42\"));\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().list_collections(\"{{session}}\", &polymorfa_sdk::business::BusinessCollectionsParameters { id: \"42\".into(), after: None, collection_limit: None, item_limit: None, width: None, height: None }, Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getBusinessEligibility": { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/eligibility", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.getEligibility(\"{{session}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().GetEligibility(ctx, \"{{session}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.get_eligibility(\"{{session}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->getEligibility(\"{{session}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.GetEligibilityAsync(\"{{session}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().get_eligibility(\"{{session}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getBusinessLinkedAccounts": { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/linked-accounts", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.getLinkedAccounts(\"{{session}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().GetLinkedAccounts(ctx, \"{{session}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.get_linked_accounts(\"{{session}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->getLinkedAccounts(\"{{session}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.GetLinkedAccountsAsync(\"{{session}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().get_linked_accounts(\"{{session}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getBusinessMerchantCompliance": { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/compliance", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.getMerchantCompliance(\"{{session}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().GetMerchantCompliance(ctx, \"{{session}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.get_merchant_compliance(\"{{session}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->getMerchantCompliance(\"{{session}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.GetMerchantComplianceAsync(\"{{session}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().get_merchant_compliance(\"{{session}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getBusinessProduct": { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/products/{productId}", + "credentialKind": "organization_api_key", + "request": { "query": { "id": "42" } }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.getProduct(\"{{session}}\", \"{{productId}}\", {\"id\": \"42\"});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().GetProduct(ctx, \"{{session}}\", \"{{productId}}\", \"42\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.get_product(\"{{session}}\", \"{{productId}}\", {\"id\": \"42\"})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->getProduct(\"{{session}}\", \"{{productId}}\", [\"id\" => \"42\"]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.GetProductAsync(\"{{session}}\", \"{{productId}}\", \"42\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().get_product(\"{{session}}\", \"{{productId}}\", \"42\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getMessageOperation": { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/operations/{operationId}", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.messages.operationStatus(\"{{session}}\", \"{{operationId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Messages().OperationStatus(ctx, \"{{session}}\", \"{{operationId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.messages.operation_status(\"{{session}}\", \"{{operationId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "messages->operationStatus(\"{{session}}\", \"{{operationId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Messages.OperationStatusAsync(\"{{session}}\", \"{{operationId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.messages().operation_status(\"{{session}}\", \"{{operationId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getOwnBusinessProfile": { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/profile", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.getProfile(\"{{session}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().GetProfile(ctx, \"{{session}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.get_profile(\"{{session}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->getProfile(\"{{session}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.GetProfileAsync(\"{{session}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().get_profile(\"{{session}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getSession": { + "family": "platform", + "method": "GET", + "path": "/platform/sessions/{sessionId}", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.sessions.retrieve(\"{{sessionId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Sessions().Retrieve(ctx, \"{{sessionId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.sessions.retrieve(\"{{sessionId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "sessions->retrieve(\"{{sessionId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Sessions.RetrieveAsync(\"{{sessionId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.sessions().retrieve(\"{{sessionId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "getSessionAccount": { + "family": "platform", + "method": "GET", + "path": "/platform/sessions/{sessionId}/me", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.sessions.account(\"{{sessionId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Sessions().Account(ctx, \"{{sessionId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.sessions.account(\"{{sessionId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "sessions->account(\"{{sessionId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Sessions.AccountAsync(\"{{sessionId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.sessions().account(\"{{sessionId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "listPlatformSessions": { + "family": "platform", + "method": "GET", + "path": "/platform/sessions", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.sessions.list();\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Sessions().List(ctx)\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.sessions.list()\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "sessions->list();\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Sessions.ListAsync();\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.sessions().list(Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "listProjectEvents": { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/events", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { Client } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new Client({ credential: { type: \"organizationApiKey\", value: \"{{POLYMORFA_API_KEY}}\" }, projectId: \"{{projectId}}\" });\n const result = await sdk.events.list();\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewProjectClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}}, \"{{projectId}}\")\n if err != nil { log.Fatal(err) }\n result, err := api.Events().List(ctx, sdk.ListEventsParams{})\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncProjectClient, Credential\n\n\nasync def main() -> None:\n async with AsyncProjectClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\"), \"{{projectId}}\") as sdk:\n result = await sdk.events.list()\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "project(\"{{projectId}}\");\n$result = $sdk->events->list();\necho json_encode($result->items, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new ProjectClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"), \"{{projectId}}\");\nvar result = await sdk.Events.ListAsync();\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{ProjectClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = ProjectClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, \"{{projectId}}\", ClientOptions::default())?;\n let result = sdk.events().list(&polymorfa_sdk::models::EventListParameters::default(), Default::default()).await?;\n println!(\"{:?}\", result.items);\n Ok(())\n}\n" + } + }, + "listProjects": { + "family": "platform", + "method": "GET", + "path": "/platform/projects", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { Client } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new Client({ credential: { type: \"organizationApiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.projects.list();\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewOrganizationClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Projects().List(ctx)\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncClient, Credential\n\n\nasync def main() -> None:\n async with AsyncClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.projects.list()\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "projects->list();\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new OrganizationClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Projects.ListAsync();\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{OrganizationClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = OrganizationClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.projects().list(Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "logoutSession": { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/logout", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.sessions.logout(\"{{sessionId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Sessions().Logout(ctx, \"{{sessionId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.sessions.logout(\"{{sessionId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "sessions->logout(\"{{sessionId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Sessions.LogoutAsync(\"{{sessionId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.sessions().logout(\"{{sessionId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "restartSession": { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/restart", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.sessions.restart(\"{{sessionId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Sessions().Restart(ctx, \"{{sessionId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.sessions.restart(\"{{sessionId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "sessions->restart(\"{{sessionId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Sessions.RestartAsync(\"{{sessionId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.sessions().restart(\"{{sessionId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "sendMessage": { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/messages/send", + "credentialKind": "organization_api_key", + "request": { + "body": { + "conversation": { "phoneNumber": "+15550001111" }, + "content": { "text": "Hello" } + } + }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.messages.send(\"{{session}}\", {\"conversation\": {\"phoneNumber\": \"+15550001111\"}, \"content\": {\"text\": \"Hello\"}});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n text := \"Hello\"\n result, err := api.Messages().Send(ctx, \"{{session}}\", sdk.SendMessageRequest{Conversation: sdk.ConversationReference{PhoneNumber: \"+15550001111\"}, Content: sdk.MessageContent{Text: &text}})\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.messages.send(\"{{session}}\", {\"conversation\": {\"phoneNumber\": \"+15550001111\"}, \"content\": {\"text\": \"Hello\"}})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "messages->send(\"{{session}}\", [\"conversation\" => [\"phoneNumber\" => \"+15550001111\"], \"content\" => [\"text\" => \"Hello\"]]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Messages.SendAsync(\"{{session}}\", new SendMessageRequest(new ConversationReference(PhoneNumber: \"+15550001111\"), new TextContent(\"Hello\")));\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.messages().send(\"{{session}}\", &polymorfa_sdk::models::SendMessageRequest::text(polymorfa_sdk::models::ConversationReference::phone(\"+15550001111\"), \"Hello\"), Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "sendReaction": { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/messages/react", + "credentialKind": "organization_api_key", + "request": { + "body": { "conversation": { "id": "42" }, "id": "43", "reaction": "👍" } + }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.messages.react(\"{{session}}\", {\"conversation\": {\"id\": \"42\"}, \"id\": \"43\", \"reaction\": \"👍\"});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Messages().React(ctx, \"{{session}}\", sdk.ReactionRequest{Conversation: sdk.ConversationReference{ID: \"42\"}, ID: \"43\", Reaction: \"👍\"})\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.messages.react(\"{{session}}\", {\"conversation\": {\"id\": \"42\"}, \"id\": \"43\", \"reaction\": \"👍\"})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "messages->react(\"{{session}}\", [\"conversation\" => [\"id\" => \"42\"], \"id\" => \"43\", \"reaction\" => \"👍\"]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Messages.ReactAsync(\"{{session}}\", new ReactRequest(new ConversationReference(Id: \"42\"), \"43\", \"👍\"));\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.messages().react(\"{{session}}\", &polymorfa_sdk::models::ReactRequest { conversation: polymorfa_sdk::models::ConversationReference::id(\"42\"), id: \"43\".into(), reaction: \"👍\".into(), transport: None }, Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "sendSeen": { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/messages/seen", + "credentialKind": "organization_api_key", + "request": { "body": { "conversation": { "id": "42" }, "id": "43" } }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.messages.markSeen(\"{{session}}\", {\"conversation\": {\"id\": \"42\"}, \"id\": \"43\"});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Messages().MarkSeen(ctx, \"{{session}}\", sdk.SeenRequest{Conversation: sdk.ConversationReference{ID: \"42\"}, ID: \"43\"})\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.messages.mark_seen(\"{{session}}\", {\"conversation\": {\"id\": \"42\"}, \"id\": \"43\"})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "messages->markSeen(\"{{session}}\", [\"conversation\" => [\"id\" => \"42\"], \"id\" => \"43\"]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Messages.MarkSeenAsync(\"{{session}}\", new SeenRequest(new ConversationReference(Id: \"42\"), \"43\"));\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.messages().mark_seen(\"{{session}}\", &polymorfa_sdk::models::SeenRequest { conversation: polymorfa_sdk::models::ConversationReference::id(\"42\"), id: \"43\".into() }, Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "setBusinessCartEnabled": { + "family": "messaging", + "method": "PATCH", + "path": "/messaging/{session}/business/catalog/cart", + "credentialKind": "organization_api_key", + "request": { "body": { "enabled": true } }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.setCartEnabled(\"{{session}}\", {\"enabled\": true});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().SetCartEnabled(ctx, \"{{session}}\", true)\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.set_cart_enabled(\"{{session}}\", {\"enabled\": True})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->setCartEnabled(\"{{session}}\", [\"enabled\" => true]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.SetCartEnabledAsync(\"{{session}}\", true);\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().set_cart_enabled(\"{{session}}\", &polymorfa_sdk::business::BusinessCartSettingRequest { enabled: true }, Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "setBusinessProductVisibility": { + "family": "messaging", + "method": "PATCH", + "path": "/messaging/{session}/business/products/{productId}/visibility", + "credentialKind": "organization_api_key", + "request": { "body": { "hidden": true } }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.business.setProductVisibility(\"{{session}}\", \"{{productId}}\", {\"hidden\": true});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Business().SetProductVisibility(ctx, \"{{session}}\", \"{{productId}}\", true)\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.business.set_product_visibility(\"{{session}}\", \"{{productId}}\", {\"hidden\": True})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "business->setProductVisibility(\"{{session}}\", \"{{productId}}\", [\"hidden\" => true]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Business.SetProductVisibilityAsync(\"{{session}}\", \"{{productId}}\", true);\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.business().set_product_visibility(\"{{session}}\", \"{{productId}}\", &polymorfa_sdk::business::BusinessProductVisibilityRequest { hidden: true }, Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "setTyping": { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/messages/typing", + "credentialKind": "organization_api_key", + "request": { + "body": { "conversation": { "id": "42" }, "state": "paused" } + }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.messages.setTyping(\"{{session}}\", {\"conversation\": {\"id\": \"42\"}, \"state\": \"paused\"});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Messages().SetTyping(ctx, \"{{session}}\", sdk.TypingRequest{Conversation: sdk.ConversationReference{ID: \"42\"}, State: \"paused\"})\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.messages.set_typing(\"{{session}}\", {\"conversation\": {\"id\": \"42\"}, \"state\": \"paused\"})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "messages->setTyping(\"{{session}}\", [\"conversation\" => [\"id\" => \"42\"], \"state\" => \"paused\"]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Messages.SetTypingAsync(\"{{session}}\", new TypingRequest(new ConversationReference(Id: \"42\"), \"paused\"));\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.messages().set_typing(\"{{session}}\", &polymorfa_sdk::models::TypingRequest { conversation: polymorfa_sdk::models::ConversationReference::id(\"42\"), id: None, state: polymorfa_sdk::models::TypingState::Paused }, Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "starMessage": { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/messages/star", + "credentialKind": "organization_api_key", + "request": { + "body": { "conversation": { "id": "42" }, "id": "43", "star": false } + }, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.messages.star(\"{{session}}\", {\"conversation\": {\"id\": \"42\"}, \"id\": \"43\", \"star\": false});\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Messages().Star(ctx, \"{{session}}\", sdk.StarRequest{Conversation: sdk.ConversationReference{ID: \"42\"}, ID: \"43\", Star: false})\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.messages.star(\"{{session}}\", {\"conversation\": {\"id\": \"42\"}, \"id\": \"43\", \"star\": False})\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "messages->star(\"{{session}}\", [\"conversation\" => [\"id\" => \"42\"], \"id\" => \"43\", \"star\" => false]);\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Messages.StarAsync(\"{{session}}\", new StarRequest(new ConversationReference(Id: \"42\"), \"43\", false));\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.messages().star(\"{{session}}\", &polymorfa_sdk::models::StarRequest { conversation: polymorfa_sdk::models::ConversationReference::id(\"42\"), id: \"43\".into(), star: false }, Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "startPlatformSession": { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/start", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.sessions.start(\"{{sessionId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Sessions().Start(ctx, \"{{sessionId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.sessions.start(\"{{sessionId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "sessions->start(\"{{sessionId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Sessions.StartAsync(\"{{sessionId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.sessions().start(\"{{sessionId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + }, + "stopPlatformSession": { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/stop", + "credentialKind": "organization_api_key", + "request": {}, + "typescript": { + "code": "import { MessagingClient } from \"@polymorfa/sdk\";\n\nasync function main(): Promise {\n const sdk = new MessagingClient({ credential: { type: \"apiKey\", value: \"{{POLYMORFA_API_KEY}}\" } });\n const result = await sdk.sessions.stop(\"{{sessionId}}\");\n console.log(result);\n}\n\nmain().catch((error: unknown) => { console.error(error); process.exitCode = 1; });\n" + }, + "go": { + "code": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n sdk \"github.com/polymorfa/sdks/packages/go\"\n)\n\nfunc main() {\n ctx := context.Background()\n api, err := sdk.NewMessagingClient(sdk.Config{Credential: sdk.Credential{Kind: sdk.OrganizationAPIKey, Value: \"{{POLYMORFA_API_KEY}}\"}})\n if err != nil { log.Fatal(err) }\n result, err := api.Sessions().Stop(ctx, \"{{sessionId}}\")\n if err != nil { log.Fatal(err) }\n fmt.Println(result)\n}\n" + }, + "python": { + "code": "import asyncio\nfrom polymorfa import AsyncMessagingClient, Credential\n\n\nasync def main() -> None:\n async with AsyncMessagingClient(Credential(\"organization_api_key\", \"{{POLYMORFA_API_KEY}}\")) as sdk:\n result = await sdk.sessions.stop(\"{{sessionId}}\")\n print(result)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n" + }, + "php": { + "code": "sessions->stop(\"{{sessionId}}\");\necho json_encode($result->data, JSON_THROW_ON_ERROR), PHP_EOL;\n" + }, + "dotnet": { + "code": "using System;\nusing Polymorfa.Sdk;\n\nusing var sdk = new MessagingClient(Credential.OrganizationApiKey(\"{{POLYMORFA_API_KEY}}\"));\nvar result = await sdk.Sessions.StopAsync(\"{{sessionId}}\");\nConsole.WriteLine(result);\n" + }, + "rust": { + "code": "use polymorfa_sdk::{MessagingClient, ClientOptions, Credential};\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() -> Result<(), Box> {\n let sdk = MessagingClient::new(Credential::organization_api_key(\"{{POLYMORFA_API_KEY}}\")?, ClientOptions::default())?;\n let result = sdk.sessions().stop(\"{{sessionId}}\", Default::default()).await?;\n println!(\"{:?}\", result);\n Ok(())\n}\n" + } + } + } +} diff --git a/contracts/doc-snippets.md b/contracts/doc-snippets.md new file mode 100644 index 00000000..688fa634 --- /dev/null +++ b/contracts/doc-snippets.md @@ -0,0 +1,124 @@ +# Documentation snippets + +`doc-snippets.json` contains self-contained server programs checked against the +public package surface. Its schema is `doc-snippets.schema.json`. Consumers pin +the **export commit** containing this file. `sourceCommit` identifies the exact +SDK source compiled, which can precede the export commit. `sourceFilesSha256` +binds the tracked package and contract inputs to that source revision, so a +later tooling or documentation commit does not create a self-reference. + +The current source includes main's separately pinned webhook phone-offline +supplement. The baseline stays at `Polymorfa-Version: 2026-09-22`, with API source +`171a9683ff45d11a4b5d3917dc428ad2722b0d99` and the two hashes in `specs`. +Separately pinned supplements retain their own versions. The generator rejects +changed source files, coverage pins and OpenAPI snapshots until the source is +rebound and the catalog is verified again. + +## Consumption + +Read `operations[operationId][language].code`. Each available entry is a complete +program making one public SDK call. Missing operation or language means use the +page's cURL example. Never synthesize a call from `coverage.json` method labels. +Match the catalog's API version, API source and specification hashes to the API +reference being rendered. A newer or mismatched page uses cURL, even if its +operation ID has the same spelling. Do not rewrite an older SDK call to fit it. + +Replace `{{POLYMORFA_API_KEY}}` with a credential of the explicit +`credentialKind`. These recipes use an **organization API key** and the matching +client principal. A project Test key does not authorize organization operations. +The docs renderer owns key selection and retrieval; this catalog never retrieves, +stores or logs a reader's key. It must escape substituted values as string +literals in the selected language. Plain text replacement of untrusted values is +unsafe, particularly for PHP dollar interpolation and quote/backslash handling. + +Every spec path parameter appears under its exact name, such as `{{session}}`, +`{{sessionId}}`, `{{projectId}}`, `{{operationId}}` and `{{collectionId}}`. +Preserve their names when consuming the artifact. Non-path request values are +synthetic examples: replace the send recipient `+15550001111`, message IDs, +business ID `42` and product IDs with values authorized for the intended request. +The `request` metadata records the minimal body and required query used by the +recipe. Its schemas and union branches are validated against the pinned OpenAPI. +No optional request fields are included in this initial catalog. + +There are **31 operations per language**, 186 complete programs. Each language's +coverage ledger contains 442 covered operations, leaving **411 omitted per +language**. The initial recipes cover text sends, message actions, session +lifecycle, business profiles/catalogs/collections and project event listing. +The remaining operations have no verified recipe in this catalog and retain +cURL fallback. Coverage of a method does not imply a verified documentation +program for it. + +## Runtime and dependencies + +These are server programs. The same runtime boundaries as the existing SDK CI +apply. Source checks use local artifacts; these instructions do not claim the +packages are available from registries. + +| Language | Program setup | +| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| TypeScript | Node 20 or 22, TypeScript with strict NodeNext modules, `@types/node`, `@polymorfa/sdk`; save as `.ts` in an ESM project. | +| Go | Go 1.24 or 1.25, module `github.com/polymorfa/sdks/packages/go`; save as `main.go`. The Go module has no manifest version; releases use `packages/go/v` tags. | +| Python | Python 3.10 or 3.13, `polymorfa-sdk`; save as `.py`. The program uses `asyncio` and closes the owned client with an async context manager. | +| PHP | PHP 8.2 or 8.4 with the SDK's required extensions, Composer autoloading for `polymorfa/sdk`; save beside `vendor/`. PHPStan verifies exported programs at its maximum level. | +| .NET | .NET 8 or 10 SDK, a `net8.0` executable project referencing `Polymorfa.Sdk`, nullable checks and warnings as errors. The program disposes its client. | +| Rust | Rust 1.85 or stable, edition 2021, `polymorfa-sdk` plus `tokio` 1 with `rt` and `macros`; save as `src/main.rs`. The current-thread runtime and owned clients drop at program end. | + +TypeScript uses the runtime's fetch transport; Go's buffered request helper owns +and closes each response body. PHP uses its synchronous owned transport. The +programs neither borrow caller-owned streaming bodies nor require a manual +transport close. + +`packages` records source names and versions. `releaseStatus: "source-only"` +means **no `install` field is emitted**. PHP's source transport version is +`0.1.0-dev.0`, which is not a Composer release constraint. The local archive +consumer uses the explicitly local `dev-doc-snippets` alias. It is not an +installation instruction or a registry version. Python retains its PEP 440 +`0.1.0.dev0` spelling. Future released catalogs need accepted publication receipts +for the exact SDK source, exact published ecosystem versions and clean registry +installation proof before adding customer installation commands. Existing API +acceptance and release-contract gates still apply. + +## Maintenance and checks + +Recipes are handwritten under `scripts/doc-snippets/`, grounded in public +signatures and existing wire tests. `recipes.mjs` defines the OpenAPI-validated +request; language modules supply explicit calls and complete program wrappers. +There is no method-label argument inference and no raw-request fallback. + +After changing SDK signatures, request serialization, public exports, package +versions, coverage or contract inputs: + +1. Inspect every affected native public method and its wire fixtures. Update its + explicit language recipe, or remove it until it can be verified. +2. Commit the SDK/package inputs, then bind `sourceCommit` to that exact source + commit. Merge current `main` first when it adds package/contract inputs that + the PR's actual merge tree will compile. Keep the API baseline and supplement + pins separate. Do not advance versions to make a check pass. +3. Run `npm run generate:doc-snippets`, `npm run check:doc-snippets` and + `npm run test:doc-snippets`. +4. In order, run `npm run verify:doc-snippets -- --negative-test` for + `typescript`, `go`, `python`, `php`, `dotnet`, `rust`. Set + `SDK_PYTHON_VERSION` to the Python runtime under test when needed. +5. Commit the verified export and pin consumers to that export commit. State + counts, omissions and source/publication status at handoff. + +The verifier materializes the **actual exported JSON code**, substitutes safe +synthetic values, and runs `tsc`, `go vet`, strict `mypy`, `php -l` plus PHPStan, +`dotnet build` and `cargo check`. It never runs the programs or sends API calls. +Every emitted program is checked; any failure fails the command and CI. +`--negative-test` replaces the exported send call with a nonexistent public +method and requires the real checker to reject it. Structural tests reject bad +placeholders, uncovered operations, invalid/missing body and queries, invalid +unions, stale SDK/API/spec provenance and unpublished install commands. + +TypeScript consumes an npm tarball, Go an extracted exact-source module with +`GOWORK=off`, Python an installed wheel, PHP a copied package archive through +Composer, .NET a local NuGet package, and Rust packaged SDK/core crates. The Rust +unpublished storage-core dependency is patched to the packaged local core, +matching existing package verification. This does not prove registry resolution. +Check artifacts stay under `~/.polymorfa-agent-work/sdk-doc-snippets-20261019/checks/`. +Set `SDK_DOC_SNIPPETS_WORK_DIR` to a task-specific directory when retaining +checker artifacts with a task receipt. The default path above remains available. +CI fetches history so `sourceCommit` exists and verifies the actual merge tree +against it. A later main change to bound inputs correctly requires rebinding +and rerunning verification. diff --git a/contracts/doc-snippets.schema.json b/contracts/doc-snippets.schema.json new file mode 100644 index 00000000..476c01da --- /dev/null +++ b/contracts/doc-snippets.schema.json @@ -0,0 +1,275 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Verified Polymorfa documentation snippets", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "sourceCommit", + "sourceFilesSha256", + "apiVersion", + "apiSourceCommit", + "specs", + "releaseStatus", + "packages", + "operations" + ], + "properties": { + "schemaVersion": { + "const": 1 + }, + "sourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "sourceFilesSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "apiVersion": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, + "apiSourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "specs": { + "type": "object", + "required": ["messaging", "platform"], + "additionalProperties": false, + "properties": { + "messaging": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "platform": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + } + }, + "releaseStatus": { + "const": "source-only" + }, + "packages": { + "type": "object", + "additionalProperties": false, + "required": ["typescript", "go", "python", "php", "dotnet", "rust"], + "properties": { + "typescript": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + } + } + }, + "go": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + } + } + }, + "python": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + } + } + }, + "php": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + } + } + }, + "dotnet": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + } + } + }, + "rust": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + } + } + } + } + }, + "operations": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": ["family", "method", "path", "credentialKind", "request"], + "properties": { + "family": { + "enum": ["messaging", "platform"] + }, + "method": { + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] + }, + "path": { + "type": "string", + "pattern": "^/" + }, + "credentialKind": { + "const": "organization_api_key" + }, + "request": { + "type": "object", + "additionalProperties": false, + "properties": { + "body": { + "type": "object" + }, + "query": { + "type": "object" + } + } + }, + "typescript": { + "type": "object", + "additionalProperties": false, + "required": ["code"], + "properties": { + "code": { + "type": "string", + "minLength": 1 + } + } + }, + "go": { + "type": "object", + "additionalProperties": false, + "required": ["code"], + "properties": { + "code": { + "type": "string", + "minLength": 1 + } + } + }, + "python": { + "type": "object", + "additionalProperties": false, + "required": ["code"], + "properties": { + "code": { + "type": "string", + "minLength": 1 + } + } + }, + "php": { + "type": "object", + "additionalProperties": false, + "required": ["code"], + "properties": { + "code": { + "type": "string", + "minLength": 1 + } + } + }, + "dotnet": { + "type": "object", + "additionalProperties": false, + "required": ["code"], + "properties": { + "code": { + "type": "string", + "minLength": 1 + } + } + }, + "rust": { + "type": "object", + "additionalProperties": false, + "required": ["code"], + "properties": { + "code": { + "type": "string", + "minLength": 1 + } + } + } + }, + "anyOf": [ + { + "required": ["typescript"] + }, + { + "required": ["go"] + }, + { + "required": ["python"] + }, + { + "required": ["php"] + }, + { + "required": ["dotnet"] + }, + { + "required": ["rust"] + } + ] + } + } + } +} diff --git a/contracts/fixtures/README.md b/contracts/fixtures/README.md new file mode 100644 index 00000000..5ef9b920 --- /dev/null +++ b/contracts/fixtures/README.md @@ -0,0 +1,28 @@ +# Shared server SDK fixtures + +`behavior.json` contains handwritten wire scenarios reviewed against the +TypeScript transport and webhook tests at SDK commit +`ff51567105b64bf6997e7330ba8eb502d875e7b9`. OpenAPI does not generate these +fixtures or runtime source. + +Start `node scripts/sdk-fixture-server.mjs --port 0`. The first output line +contains the loopback URL. Set the per-request header +`x-polymorfa-fixture: ` to select the expected wire scenario. +Alternatively, start with `--scenario ` and use the normal +loopback base URL with no selection header. Client URL resolution remains +unchanged. Each scenario owns its request counter, so languages can share one process. +Reset with `POST /__fixtures//reset`. Inspect counters and +redacted mismatches with `GET /__fixtures//state`. + +Tests must invoke the public client method, assert the decoded result or typed +error and metadata, then assert the expected attempt count and no mismatches. +Passing a route manifest alone is not coverage. Operation-specific tests live +with each handwritten client and supplement these transport scenarios. + +Native webhooks sign only the exact raw body with HMAC-SHA256. They do not +have timestamps or intrinsic replay prevention; applications deduplicate the +verified event ID. Local-forward signatures use their separate timestamped +protocol. The webhook cases explicitly distinguish the two. + +All credentials and webhook secrets in this directory are synthetic fixtures. +Never use them against a real environment. diff --git a/contracts/fixtures/behavior.json b/contracts/fixtures/behavior.json new file mode 100644 index 00000000..69568735 --- /dev/null +++ b/contracts/fixtures/behavior.json @@ -0,0 +1,1200 @@ +{ + "schemaVersion": 1, + "sourceSdkCommit": "ff51567105b64bf6997e7330ba8eb502d875e7b9", + "apiVersion": "2026-09-22", + "organizationCredential": "pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "projectCredential": "pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "scenarios": [ + { + "id": "sessions-list", + "request": { + "method": "GET", + "path": "/platform/sessions", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + }, + "query": {} + }, + "responses": [ + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": [ + { + "_id": "number_fixture", + "_creationTime": 1787133600000, + "projectId": "project_fixture", + "sessionId": "session_fixture", + "name": "support", + "phone": null, + "platform": null, + "isBusiness": false, + "testMode": true, + "tierOverride": null, + "status": "CONNECTED", + "messageCount": 0, + "lastActiveAt": null, + "paidUntil": null + } + ] + } + } + ], + "outcome": { + "attempts": 1 + } + }, + { + "id": "query-encoding", + "request": { + "method": "GET", + "path": "/platform/sessions", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + }, + "query": { + "limit": "25", + "active": "true", + "tag": ["alpha", "beta"], + "search": "a & b" + } + }, + "responses": [ + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + } + } + ], + "outcome": { + "attempts": 1 + } + }, + { + "id": "message-send", + "request": { + "method": "POST", + "path": "/messaging/support/messages/send", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22", + "idempotency-key": "send-fixture" + }, + "body": { + "conversation": { + "phoneNumber": "+15551234567" + }, + "content": { + "text": "Hello" + } + } + }, + "responses": [ + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "success": true, + "data": { + "id": "msg_fixture", + "whatsapp_ids": { + "linked_devices": "provider_fixture" + }, + "conversation": { + "id": "user_fixture" + }, + "timestamp": "2026-08-19T10:00:00Z", + "status": "sent", + "type": "text" + } + } + } + ], + "outcome": { + "attempts": 1 + } + }, + { + "id": "safe-retry", + "request": { + "method": "GET", + "path": "/platform/sessions", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + }, + "query": {} + }, + "responses": [ + { + "status": 503, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + } + }, + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + } + } + ], + "outcome": { + "attempts": 2 + } + }, + { + "id": "rate-limit-retry", + "request": { + "method": "GET", + "path": "/platform/sessions", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + }, + "query": {} + }, + "responses": [ + { + "status": 429, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "data": { + "id": "fixture" + } + } + }, + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + } + } + ], + "outcome": { + "attempts": 2 + } + }, + { + "id": "unsafe-no-retry", + "request": { + "method": "POST", + "path": "/messaging/fixtures/mutation", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + }, + "body": { + "fixture": true + } + }, + "responses": [ + { + "status": 503, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + } + }, + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + } + } + ], + "outcome": { + "attempts": 1, + "error": "server" + } + }, + { + "id": "idempotent-retry", + "request": { + "method": "POST", + "path": "/messaging/fixtures/mutation", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22", + "idempotency-key": "fixture-stable-key" + }, + "body": { + "fixture": true + } + }, + "responses": [ + { + "status": 503, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "data": { + "id": "fixture" + } + } + }, + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + } + } + ], + "outcome": { + "attempts": 2 + } + }, + { + "id": "replayed-final", + "request": { + "method": "POST", + "path": "/messaging/fixtures/mutation", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22", + "idempotency-key": "fixture-stable-key" + }, + "body": { + "fixture": true + } + }, + "responses": [ + { + "status": 503, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "idempotent-replayed": "true" + }, + "body": { + "data": { + "id": "fixture" + } + } + }, + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + } + } + ], + "outcome": { + "attempts": 1, + "error": "server" + } + }, + { + "id": "error-400", + "request": { + "method": "GET", + "path": "/messaging/fixtures/error", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 400, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "error": { + "code": "fixture_error", + "message": "Fixture rejection", + "request_id": "req_body", + "request_log_url": "https://console.polymorfa.com/logs/fixture" + }, + "docs": "https://docs.polymorfa.com/errors" + } + } + ], + "outcome": { + "attempts": 1, + "maxNetworkRetries": 0, + "error": "validation", + "code": "fixture_error", + "requestId": "req_body", + "metadataRequestId": "req_fixture" + } + }, + { + "id": "error-401", + "request": { + "method": "GET", + "path": "/messaging/fixtures/error", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 401, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "error": { + "code": "fixture_error", + "message": "Fixture rejection", + "request_id": "req_body", + "request_log_url": "https://console.polymorfa.com/logs/fixture" + }, + "docs": "https://docs.polymorfa.com/errors" + } + } + ], + "outcome": { + "attempts": 1, + "maxNetworkRetries": 0, + "error": "authentication", + "code": "fixture_error", + "requestId": "req_body", + "metadataRequestId": "req_fixture" + } + }, + { + "id": "error-402", + "request": { + "method": "GET", + "path": "/messaging/fixtures/error", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 402, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "error": { + "code": "fixture_error", + "message": "Fixture rejection", + "request_id": "req_body", + "request_log_url": "https://console.polymorfa.com/logs/fixture" + }, + "docs": "https://docs.polymorfa.com/errors" + } + } + ], + "outcome": { + "attempts": 1, + "maxNetworkRetries": 0, + "error": "payment_required", + "code": "fixture_error", + "requestId": "req_body", + "metadataRequestId": "req_fixture" + } + }, + { + "id": "error-403", + "request": { + "method": "GET", + "path": "/messaging/fixtures/error", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 403, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "error": { + "code": "fixture_error", + "message": "Fixture rejection", + "request_id": "req_body", + "request_log_url": "https://console.polymorfa.com/logs/fixture" + }, + "docs": "https://docs.polymorfa.com/errors" + } + } + ], + "outcome": { + "attempts": 1, + "maxNetworkRetries": 0, + "error": "authorization", + "code": "fixture_error", + "requestId": "req_body", + "metadataRequestId": "req_fixture" + } + }, + { + "id": "error-404", + "request": { + "method": "GET", + "path": "/messaging/fixtures/error", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 404, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "error": { + "code": "fixture_error", + "message": "Fixture rejection", + "request_id": "req_body", + "request_log_url": "https://console.polymorfa.com/logs/fixture" + }, + "docs": "https://docs.polymorfa.com/errors" + } + } + ], + "outcome": { + "attempts": 1, + "maxNetworkRetries": 0, + "error": "not_found", + "code": "fixture_error", + "requestId": "req_body", + "metadataRequestId": "req_fixture" + } + }, + { + "id": "error-409", + "request": { + "method": "GET", + "path": "/messaging/fixtures/error", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 409, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "error": { + "code": "fixture_error", + "message": "Fixture rejection", + "request_id": "req_body", + "request_log_url": "https://console.polymorfa.com/logs/fixture" + }, + "docs": "https://docs.polymorfa.com/errors" + } + } + ], + "outcome": { + "attempts": 1, + "maxNetworkRetries": 0, + "error": "conflict", + "code": "fixture_error", + "requestId": "req_body", + "metadataRequestId": "req_fixture" + } + }, + { + "id": "error-422", + "request": { + "method": "GET", + "path": "/messaging/fixtures/error", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 422, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "error": { + "code": "fixture_error", + "message": "Fixture rejection", + "request_id": "req_body", + "request_log_url": "https://console.polymorfa.com/logs/fixture" + }, + "docs": "https://docs.polymorfa.com/errors" + } + } + ], + "outcome": { + "attempts": 1, + "maxNetworkRetries": 0, + "error": "validation", + "code": "fixture_error", + "requestId": "req_body", + "metadataRequestId": "req_fixture" + } + }, + { + "id": "error-429", + "request": { + "method": "GET", + "path": "/messaging/fixtures/error", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 429, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "error": { + "code": "fixture_error", + "message": "Fixture rejection", + "request_id": "req_body", + "request_log_url": "https://console.polymorfa.com/logs/fixture" + }, + "docs": "https://docs.polymorfa.com/errors" + } + } + ], + "outcome": { + "attempts": 1, + "maxNetworkRetries": 0, + "error": "rate_limit", + "code": "fixture_error", + "requestId": "req_body", + "metadataRequestId": "req_fixture" + } + }, + { + "id": "error-500", + "request": { + "method": "GET", + "path": "/messaging/fixtures/error", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 500, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0" + }, + "body": { + "error": { + "code": "fixture_error", + "message": "Fixture rejection", + "request_id": "req_body", + "request_log_url": "https://console.polymorfa.com/logs/fixture" + }, + "docs": "https://docs.polymorfa.com/errors" + } + } + ], + "outcome": { + "attempts": 1, + "maxNetworkRetries": 0, + "error": "server", + "code": "fixture_error", + "requestId": "req_body", + "metadataRequestId": "req_fixture" + } + }, + { + "id": "html-error", + "request": { + "method": "GET", + "path": "/messaging/fixtures/error", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 502, + "headers": { + "content-type": "text/html", + "x-request-id": "req_proxy" + }, + "rawBody": "proxy failure" + } + ], + "outcome": { + "attempts": 1, + "maxNetworkRetries": 0, + "error": "server" + } + }, + { + "id": "timeout", + "request": { + "method": "GET", + "path": "/platform/sessions", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + }, + "query": {} + }, + "responses": [ + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + }, + "delayMs": 250 + } + ], + "outcome": { + "attempts": 1, + "timeoutMs": 20, + "maxNetworkRetries": 0, + "error": "timeout" + } + }, + { + "id": "cancellation", + "request": { + "method": "GET", + "path": "/platform/sessions", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + }, + "query": {} + }, + "responses": [ + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + }, + "delayMs": 250 + } + ], + "outcome": { + "attempts": 1, + "cancelAfterMs": 20, + "maxNetworkRetries": 0, + "error": "cancelled" + } + }, + { + "id": "no-content", + "request": { + "method": "DELETE", + "path": "/messaging/fixtures/item", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22" + } + }, + "responses": [ + { + "status": 204, + "headers": { + "x-request-id": "req_empty" + } + } + ], + "outcome": { + "attempts": 1, + "data": null + } + }, + { + "id": "request-version", + "request": { + "method": "GET", + "path": "/platform/sessions", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-10-01" + }, + "query": {} + }, + "responses": [ + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-10-01" + }, + "body": { + "data": { + "id": "fixture" + } + } + } + ], + "outcome": { + "attempts": 1 + } + }, + { + "id": "operation-admitted-final", + "request": { + "method": "POST", + "path": "/messaging/fixtures/mutation", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22", + "idempotency-key": "fixture-stable-key" + }, + "body": { + "fixture": true + } + }, + "responses": [ + { + "status": 503, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0", + "x-polymorfa-operation-id": "op_fixture" + }, + "body": { + "data": { + "id": "fixture" + } + } + }, + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22" + }, + "body": { + "data": { + "id": "fixture" + } + } + } + ], + "outcome": { + "attempts": 1, + "error": "server", + "operationId": "op_fixture" + } + }, + { + "id": "operation-admitted-body-timeout", + "request": { + "method": "POST", + "path": "/messaging/fixtures/mutation", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22", + "idempotency-key": "fixture-stable-key" + }, + "body": { + "fixture": true + } + }, + "responses": [ + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0", + "x-polymorfa-operation-id": "op_fixture" + }, + "body": { + "data": { + "id": "fixture" + } + }, + "bodyDelayMs": 250 + } + ], + "outcome": { + "attempts": 1, + "error": "timeout", + "operationId": "op_fixture", + "metadataRequestId": "req_fixture", + "timeoutMs": 75 + } + }, + { + "id": "operation-admitted-body-truncated", + "request": { + "method": "POST", + "path": "/messaging/fixtures/mutation", + "headers": { + "authorization": "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "polymorfa-version": "2026-09-22", + "idempotency-key": "fixture-stable-key" + }, + "body": { + "fixture": true + } + }, + "responses": [ + { + "status": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req_fixture", + "polymorfa-version": "2026-09-22", + "retry-after": "0", + "x-polymorfa-operation-id": "op_fixture" + }, + "body": { + "data": { + "id": "fixture" + } + }, + "disconnectAfterBytes": 6 + } + ], + "outcome": { + "attempts": 1, + "error": "connection", + "operationId": "op_fixture", + "metadataRequestId": "req_fixture", + "timeoutMs": 1000 + } + } + ], + "webhooks": [ + { + "id": "native-unknown", + "protocol": "native", + "body": "{\"id\":\"evt_fixture\",\"session\":\"support\",\"timestamp\":\"2026-08-19T10:00:00Z\",\"event\":\"future.ready\",\"payload\":{\"capability\":42}}", + "secret": "fixture-secret", + "signature": "8b9682c1aee07cf45ce4b83983c2c224000ae4f8289c1ff38169bec4a52c6edd", + "valid": true, + "event": "future.ready" + }, + { + "id": "native-prefixed", + "protocol": "native", + "body": "{\"id\":\"evt_fixture\",\"session\":\"support\",\"timestamp\":\"2026-08-19T10:00:00Z\",\"event\":\"future.ready\",\"payload\":{\"capability\":42}}", + "secret": "fixture-secret", + "signature": "sha256=8b9682c1aee07cf45ce4b83983c2c224000ae4f8289c1ff38169bec4a52c6edd", + "valid": true + }, + { + "id": "native-replay", + "protocol": "native", + "body": "{\"id\":\"evt_fixture\",\"session\":\"support\",\"timestamp\":\"2026-08-19T10:00:00Z\",\"event\":\"future.ready\",\"payload\":{\"capability\":42}}", + "secret": "fixture-secret", + "signature": "8b9682c1aee07cf45ce4b83983c2c224000ae4f8289c1ff38169bec4a52c6edd", + "valid": true, + "note": "A repeated native signature stays valid; application event-ID deduplication is separate." + }, + { + "id": "native-mutated", + "protocol": "native", + "body": "{\"id\":\"evt_fixture\",\"session\":\"support\",\"timestamp\":\"2026-08-19T10:00:00Z\",\"event\":\"future.ready\",\"payload\":{\"capability\":42}} ", + "secret": "fixture-secret", + "signature": "8b9682c1aee07cf45ce4b83983c2c224000ae4f8289c1ff38169bec4a52c6edd", + "valid": false + }, + { + "id": "native-malformed", + "protocol": "native", + "body": "{\"id\":\"evt_fixture\",\"session\":\"support\",\"timestamp\":\"2026-08-19T10:00:00Z\",\"event\":\"future.ready\",\"payload\":{\"capability\":42}}", + "secret": "fixture-secret", + "signature": "not-hex", + "valid": false + }, + { + "id": "native-empty-secret", + "protocol": "native", + "body": "{\"id\":\"evt_fixture\",\"session\":\"support\",\"timestamp\":\"2026-08-19T10:00:00Z\",\"event\":\"future.ready\",\"payload\":{\"capability\":42}}", + "secret": "", + "signature": "8b9682c1aee07cf45ce4b83983c2c224000ae4f8289c1ff38169bec4a52c6edd", + "valid": false + }, + { + "id": "local-10", + "protocol": "local", + "body": "{\"id\":\"evt_fixture\",\"session\":\"support\",\"timestamp\":\"2026-08-19T10:00:00Z\",\"event\":\"future.ready\",\"payload\":{\"capability\":42}}", + "secret": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc", + "signature": "t=1787133600,v1=39cfb3e51cfd49878194a4f47c3277877e8b68d78fe90822f07a04f6b73b154b", + "nowUnixSeconds": 1787133610, + "toleranceSeconds": 300, + "valid": true + }, + { + "id": "local-300", + "protocol": "local", + "body": "{\"id\":\"evt_fixture\",\"session\":\"support\",\"timestamp\":\"2026-08-19T10:00:00Z\",\"event\":\"future.ready\",\"payload\":{\"capability\":42}}", + "secret": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc", + "signature": "t=1787133600,v1=39cfb3e51cfd49878194a4f47c3277877e8b68d78fe90822f07a04f6b73b154b", + "nowUnixSeconds": 1787133900, + "toleranceSeconds": 300, + "valid": true + }, + { + "id": "local-301", + "protocol": "local", + "body": "{\"id\":\"evt_fixture\",\"session\":\"support\",\"timestamp\":\"2026-08-19T10:00:00Z\",\"event\":\"future.ready\",\"payload\":{\"capability\":42}}", + "secret": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc", + "signature": "t=1787133600,v1=39cfb3e51cfd49878194a4f47c3277877e8b68d78fe90822f07a04f6b73b154b", + "nowUnixSeconds": 1787133901, + "toleranceSeconds": 300, + "valid": false + }, + { + "id": "local--301", + "protocol": "local", + "body": "{\"id\":\"evt_fixture\",\"session\":\"support\",\"timestamp\":\"2026-08-19T10:00:00Z\",\"event\":\"future.ready\",\"payload\":{\"capability\":42}}", + "secret": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc", + "signature": "t=1787133600,v1=39cfb3e51cfd49878194a4f47c3277877e8b68d78fe90822f07a04f6b73b154b", + "nowUnixSeconds": 1787133299, + "toleranceSeconds": 300, + "valid": false + } + ], + "configuration": [ + { + "id": "organization-valid", + "client": "platform", + "credentialType": "organizationApiKey", + "credential": "pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "valid": true + }, + { + "id": "messaging-org-valid", + "client": "messaging", + "credentialType": "apiKey", + "credential": "pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "valid": true + }, + { + "id": "project-valid", + "client": "platform", + "credentialType": "projectToken", + "credential": "pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "projectId": "project_fixture", + "valid": true + }, + { + "id": "project-missing-id", + "client": "platform", + "credentialType": "projectToken", + "credential": "pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "valid": false, + "field": "projectId" + }, + { + "id": "project-noncanonical-tail", + "client": "platform", + "credentialType": "projectToken", + "credential": "pmfa_pt_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaB", + "projectId": "project_fixture", + "valid": false, + "field": "credential" + }, + { + "id": "organization-short", + "client": "platform", + "credentialType": "organizationApiKey", + "credential": "pmfa_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": false, + "field": "credential" + }, + { + "id": "organization-wrong-principal", + "client": "platform", + "credentialType": "organizationApiKey", + "credential": "pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "valid": false, + "field": "credential" + }, + { + "id": "project-wrong-principal", + "client": "platform", + "credentialType": "projectToken", + "credential": "pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "projectId": "project_fixture", + "valid": false, + "field": "credential" + }, + { + "id": "listener-refused", + "client": "platform", + "credentialType": "organizationApiKey", + "credential": "pmfa_ls_fixture", + "valid": false, + "field": "credential" + }, + { + "id": "client-token-refused-platform", + "client": "platform", + "credentialType": "projectToken", + "credential": "pmfa_ct_Zml4dHVyZQ", + "projectId": "project_fixture", + "valid": false, + "field": "credential" + }, + { + "id": "client-token-valid-messaging", + "client": "messaging", + "credentialType": "clientToken", + "credential": "pmfa_ct_Zml4dHVyZQ", + "valid": true + }, + { + "id": "invalid-base-url", + "client": "platform", + "credentialType": "organizationApiKey", + "credential": "pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "baseUrl": "https://user:password@example.invalid", + "valid": false, + "field": "baseUrl" + }, + { + "id": "negative-retries", + "client": "platform", + "credentialType": "organizationApiKey", + "credential": "pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "maxNetworkRetries": -1, + "valid": false + }, + { + "id": "zero-timeout", + "client": "platform", + "credentialType": "organizationApiKey", + "credential": "pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "timeoutMs": 0, + "valid": false + } + ] +} diff --git a/contracts/fixtures/official-group-picture.jpg b/contracts/fixtures/official-group-picture.jpg new file mode 100644 index 00000000..bd9c2478 Binary files /dev/null and b/contracts/fixtures/official-group-picture.jpg differ diff --git a/contracts/fixtures/official-round2.json b/contracts/fixtures/official-round2.json new file mode 100644 index 00000000..ab320e2d --- /dev/null +++ b/contracts/fixtures/official-round2.json @@ -0,0 +1,62 @@ +{ + "sourceCommit": "9bb1e2ed92d8b4a07ff727f8e7ae185498f0868f", + "published": false, + "readinessCodes": [ + "meta_credentials_invalid", + "meta_phone_not_registered", + "meta_webhook_not_subscribed", + "meta_payment_method_missing", + "meta_quality_flagged", + "meta_readiness_unknown" + ], + "contact": { + "id": "new", + "bsuid": "new-b", + "parentBsuid": "new-p", + "identityChange": { + "reason": "user_id_changed", + "messageId": "m", + "previous": { + "id": "old", + "bsuid": "old-b", + "parentBsuid": "old-p" + } + } + }, + "order": { + "catalogId": "123", + "items": [ + { + "productRetailerId": "p", + "quantity": 2, + "itemPrice": 0.1, + "currency": "USD" + } + ] + }, + "batch": { + "object": "event_batch", + "version": 1, + "id": "00000000-0000-4000-8000-000000000001", + "events": [ + { + "id": "e", + "session": "", + "timestamp": "2026-10-22T00:00:00Z", + "event": "future.event", + "payload": { + "future": true + } + } + ] + }, + "picture": { + "path": "contracts/fixtures/official-group-picture.jpg", + "width": 192, + "height": 192, + "format": "jpeg", + "bytes": 1171, + "sha256": "ac170354f2f91e79b66affabed5d344ae943350b83ae52dc3850e413bd2504bf", + "source": "Synthetic solid image encoded with Go image/jpeg; no customer or provider data." + } +} diff --git a/contracts/fixtures/whatsapp-media.json b/contracts/fixtures/whatsapp-media.json new file mode 100644 index 00000000..4d02e636 --- /dev/null +++ b/contracts/fixtures/whatsapp-media.json @@ -0,0 +1,66 @@ +{ + "sourceSdkCommit": "ff51567105b64bf6997e7330ba8eb502d875e7b9", + "derivation": "Node crypto hkdfSync/createCipheriv/createHmac matching pinned TS test/whatsapp-media.test.ts", + "fixtures": [ + { + "kind": "image", + "plaintext": "506f6c796d6f72666120696d616765207665726966696564206d656469612066697874757265", + "encrypted": "0f114f756129a8f4e2b974b3d5ba3e1c0eb8472a11dd46c88fa4f0c57bf52fd915b105269eec1db7299268242c1580e55b3d7479ac63b1a278dc", + "mediaKey": "0101010101010101010101010101010101010101010101010101010101010101", + "fileSha256": "54ec0136adb774da821c8232687fbb75f7b0ff5153ac7c126615b4599da37828", + "fileEncSha256": "c197062f5989117b02e4eaeb78db3a5502610964de98a2a4af22e7b554c9d471", + "iv": "26cd9086031e8165f56558c2ffbf64b4", + "cipherKey": "784afd482d9bf6f33c90f0e5000f5c7cd57c24c35feb80176a2d9132009c44f4", + "macKey": "fe7555414f6522cdcfe0dbb536b20f0509ac02ab9e2ce7ffae6bfd742246e1e2", + "descriptor": "pQEBAgMEigECCAEKImh0dHBzOi8vbW1nLndoYXRzYXBwLm5ldC9pbWFnZS5lbmMSDWltYWdlL2ZpeHR1cmUiIFTsATatt3TaghyCMmh/u3X3sP9RU6x8EmYVtFmdo3goKCZCIAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBSiDBlwYvWYkRewLk6ut42zpVAmEJZN6YoqSvIue1VMnUcVoKL2ltYWdlLmVuYw==" + }, + { + "kind": "video", + "plaintext": "506f6c796d6f72666120766964656f207665726966696564206d656469612066697874757265", + "encrypted": "bd04f87bc9695bfa50859e920cbffdea753f214256b3e89494a72664e3a16d979914fd2adac0f30eba1af16d5ee13eb94aad3826805002c87c2e", + "mediaKey": "0202020202020202020202020202020202020202020202020202020202020202", + "fileSha256": "d52e2795688f17c7e5c6bdae3ecbd6567942fb6b794d9568d266871dfde88c23", + "fileEncSha256": "e928d2abb870db7c2a0746d0e046917ec5a8917318227aec907c95882f174d14", + "iv": "0372d151c905ba1cbce83c565ad8ac0b", + "cipherKey": "fb11ef1a7d475675d873df03ad4ca0eef423efa092ea6a7a0740b5c930f85231", + "macKey": "b142cf5ba336b3dbd4a734bdc4fa8d30fcc34be7510a1dee8890f7c148511415", + "descriptor": "pQEBAgMEigECCAEKImh0dHBzOi8vbW1nLndoYXRzYXBwLm5ldC92aWRlby5lbmMSDXZpZGVvL2ZpeHR1cmUaINUuJ5VojxfH5ca9rj7L1lZ5QvtreU2VaNJmhx396IwjICYyIAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICWiDpKNKruHDbfCoHRtDgRpF+xaiRcxgieuyQfJWILxdNFGoKL3ZpZGVvLmVuYw==" + }, + { + "kind": "audio", + "plaintext": "506f6c796d6f72666120617564696f207665726966696564206d656469612066697874757265", + "encrypted": "de7ddda5185a71f605116dbda055cefdc1511bb4469765b78429002defb183dd735595509845efbf379e1d99e7b0319dc4b4347f2c702bc75881", + "mediaKey": "0303030303030303030303030303030303030303030303030303030303030303", + "fileSha256": "692eb64b3b44d51dfd1baf52d51b37d49537fa8c4f8f70fd6b08179d044421d3", + "fileEncSha256": "f13b2971e346a24b1b554100cf1e12cb585b1c1337f46af66144df043e837075", + "iv": "077194a55afb9317b288e97c283beb6a", + "cipherKey": "2fd02b7b093bbd676ddb1e8859193141efe4cf4c5ddd9c510db64ed7bb60aa28", + "macKey": "69b026c5ef17278cecf0a12734ef93ffa71e278ef9601b5ba94550f697b4543b", + "descriptor": "pQEBAgMEigECCAEKImh0dHBzOi8vbW1nLndoYXRzYXBwLm5ldC9hdWRpby5lbmMSDWF1ZGlvL2ZpeHR1cmUaIGkutks7RNUd/RuvUtUbN9SVN/qMT49w/WsIF50ERCHTICY6IAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDQiDxOylx40aiSxtVQQDPHhLLWFscEzf0avZhRN8EPoNwdUoKL2F1ZGlvLmVuYw==" + }, + { + "kind": "document", + "plaintext": "506f6c796d6f72666120646f63756d656e74207665726966696564206d656469612066697874757265", + "encrypted": "157e5658c6f38d966358477afc2c257f8699c026f4fcbbb02eb866cc620e831b099f33414ee91c1e3267a2c31a12cc6d8fcf96fade1399b3b2a3", + "mediaKey": "0404040404040404040404040404040404040404040404040404040404040404", + "fileSha256": "928bca968cbe3658919d7cbce6b5fd3a76b1ba155d33d338b79be5dc4d758d95", + "fileEncSha256": "c0ab2d1eea401e127c0f71084b454e8a284a6d42ec2f82ae8cfe6b6beb4121aa", + "iv": "dc5be2c57fda49e97fabd5cf4dc38f24", + "cipherKey": "902866eed56aa1f62e4eeca1a6fb29cbe7b15719c64cbfc839407198edc1ffb2", + "macKey": "e3da6b2720ffc784f22d2e0b78c0b651d2fcf14e411d65b49309c537a8c42cce", + "descriptor": "pQEBAgMEigECCAEKJWh0dHBzOi8vbW1nLndoYXRzYXBwLm5ldC9kb2N1bWVudC5lbmMSEGRvY3VtZW50L2ZpeHR1cmUiIJKLypaMvjZYkZ18vOa1/Tp2sboVXTPTOLeb5dxNdY2VKCk6IAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQESiDAqy0e6kAeEnwPcQhLRU6KKEptQuwvgq6M/mtr60EhqlINL2RvY3VtZW50LmVuY0IKcmVwb3J0LnBkZg==" + }, + { + "kind": "sticker", + "plaintext": "506f6c796d6f72666120737469636b6572207665726966696564206d656469612066697874757265", + "encrypted": "0579add5f27bdc73c16ff2af9bd37c41e8f1b80998f3f437f25a24384c9ebf7be650092ba3a4910cb14fc2561c2702f77c6c7c163e59494b9956", + "mediaKey": "0505050505050505050505050505050505050505050505050505050505050505", + "fileSha256": "8d72bb4cbffaaa152f88e0f1851ce71d29cbb8d4624fcd43f4e0cf829884ddd7", + "fileEncSha256": "c4fbc9b7b19bea179ef8b4bce40db92f398c1a9f0df4000a1d7d71b173ffeda0", + "iv": "745c6cebf27831d38dee5c5daeac6d44", + "cipherKey": "fee0e9837bd18cc17ee4dd418894233519d5626ec7dd593fedeedb47796c2dd9", + "macKey": "102a80da030ff81c49116ecbc01673f5c201145603d7e67b5d9801cbd1c8eb2b", + "descriptor": "pQEBAgMEigECCAEKJGh0dHBzOi8vbW1nLndoYXRzYXBwLm5ldC9zdGlja2VyLmVuYyoPc3RpY2tlci9maXh0dXJlEiCNcrtMv/qqFS+I4PGFHOcdKcu41GJPzUP04M+CmITd10goIiAFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBRogxPvJt7Gb6hee+LS85A25LzmMGp8N9AAKHX1xsXP/7aBCDC9zdGlja2VyLmVuYw==" + } + ] +} diff --git a/contracts/machine-credentials.source.json b/contracts/machine-credentials.source.json new file mode 100644 index 00000000..8378150d --- /dev/null +++ b/contracts/machine-credentials.source.json @@ -0,0 +1,8 @@ +{ + "repository": "polymorfa/polymorfa", + "commit": "e95d02da674da8242453c2a41efd48883fa04734", + "path": "apps/api/testdata/machine-credentials/v1.json", + "blob": "a40eb08683ff5f6eae62b821992fa1431df9a5db", + "sha256": "7694d8eb61dca6aed69f41392d2e5fae585084e330b276ee6a71844ef9e2365d", + "bytes": 53840 +} diff --git a/contracts/machine-credentials.v1.json b/contracts/machine-credentials.v1.json new file mode 100644 index 00000000..a40eb086 --- /dev/null +++ b/contracts/machine-credentials.v1.json @@ -0,0 +1,516 @@ +{ + "testOnlySeedHex": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "now": 1792195200000, + "key": { + "kid": "mc1", + "purpose": "machine-credentials", + "algorithm": "Ed25519", + "activatesAt": 0, + "retiresAt": null, + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=\n-----END PUBLIC KEY-----\n" + }, + "context": { + "environment": "test", + "issuer": "https://platform.example.test", + "audience": "polymorfa-api" + }, + "formatRevision": 1, + "crcAlgorithm": "CRC-32/ISO-HDLC", + "kidRegistry": [ + { + "kid": "mc1", + "environment": "test" + } + ], + "opaque": [ + { + "name": "key", + "kind": "key", + "environment": "test", + "secretHex": "000102030405060708090a0b0c0d0e0f10111213", + "checksumHex": "3bddffa4", + "body": "AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "token": "pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "tokenLength": 37, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e6f70617175652e7368613235360074657374006b657900000102030405060708090a0b0c0d0e0f10111213", + "tokenHashHex": "6e10862b045a45183f5173969a2f5aa7cb93841ddbda52f32e4c55a9b3a9dded" + }, + { + "name": "project", + "kind": "project", + "environment": "test", + "secretHex": "000102030405060708090a0b0c0d0e0f10111213", + "checksumHex": "3bddffa4", + "body": "AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "token": "pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k", + "tokenLength": 40, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e6f70617175652e73686132353600746573740070726f6a65637400000102030405060708090a0b0c0d0e0f10111213", + "tokenHashHex": "a69d6a7ca7dc5052993dc6bdb0f7a3483543bc2ca9c4e5cbd265535b6722211a" + } + ], + "client": { + "testOnlySeedHex": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=\n-----END PUBLIC KEY-----\n", + "keyMetadata": { + "kid": "mc1", + "purpose": "machine-credentials", + "algorithm": "Ed25519", + "activatesAt": 0, + "retiresAt": null, + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=\n-----END PUBLIC KEY-----\n" + }, + "cases": [ + { + "name": "project-issued", + "claims": { + "id": "33333333-3333-4333-8333-333333333333", + "kind": "client", + "issuedAt": 1792195200000, + "ttlSecs": 3600, + "orgId": "44444444-4444-4444-8444-444444444444", + "sessionName": "test-number", + "ephemeralId": "browser", + "issuerKind": "project", + "issuerId": "22222222-2222-4222-8222-222222222222", + "permissionCeiling": "18446744073709551615", + "testNumbersOnly": false + }, + "payloadHex": "01036d633133333333333343338333333333333333000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720122222222222242228222222222222222ffffffffffffffff00", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633133333333333343338333333333333333000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720122222222222242228222222222222222ffffffffffffffff00", + "signatureHex": "535f855c526ae4e6e2a36398271f4d0b72b58ac3d7967b5f0f544809b3567fe3d94b61db836ac404a17814d10c55e66b72e6be50e4cf481bab58bc2bdb50810e", + "token": "pmfa_ct_AQNtYzEzMzMzMzNDM4MzMzMzMzMzAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyASIiIiIiIkIigiIiIiIiIiL__________wBTX4VcUmrk5uKjY5gnH00LcrWKw9eWe18PVEgJs1Z_49lLYduDasQEoXgU0QxV5mty5r5Q5M9IG6tYvCvbUIEO", + "tokenLength": 220, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633133333333333343338333333333333333000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720122222222222242228222222222222222ffffffffffffffff00535f855c526ae4e6e2a36398271f4d0b72b58ac3d7967b5f0f544809b3567fe3d94b61db836ac404a17814d10c55e66b72e6be50e4cf481bab58bc2bdb50810e", + "tokenHashHex": "db38b609c61c987c5e4b18d2ccdc990ace4cb9818db503ace80633321b96336d" + }, + { + "name": "project-issued-test-only", + "claims": { + "id": "33333333-3333-4333-8333-333333333333", + "kind": "client", + "issuedAt": 1792195200000, + "ttlSecs": 3600, + "orgId": "44444444-4444-4444-8444-444444444444", + "sessionName": "test-number", + "ephemeralId": "browser", + "issuerKind": "project", + "issuerId": "22222222-2222-4222-8222-222222222222", + "permissionCeiling": "18446744073709551615", + "testNumbersOnly": true + }, + "payloadHex": "01036d633133333333333343338333333333333333000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720122222222222242228222222222222222ffffffffffffffff02", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633133333333333343338333333333333333000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720122222222222242228222222222222222ffffffffffffffff02", + "signatureHex": "5ce274c4ee5bb2d9b184e60e02b175988cb38708c7bf80ff667de89d0a31192fb1597cc1cc21be7d907c07295fffe9ed53e02f463b5e0a617333ee77650a1307", + "token": "pmfa_ct_AQNtYzEzMzMzMzNDM4MzMzMzMzMzAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyASIiIiIiIkIigiIiIiIiIiL__________wJc4nTE7luy2bGE5g4CsXWYjLOHCMe_gP9mfeidCjEZL7FZfMHMIb59kHwHKV__6e1T4C9GO14KYXMz7ndlChMH", + "tokenLength": 220, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633133333333333343338333333333333333000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720122222222222242228222222222222222ffffffffffffffff025ce274c4ee5bb2d9b184e60e02b175988cb38708c7bf80ff667de89d0a31192fb1597cc1cc21be7d907c07295fffe9ed53e02f463b5e0a617333ee77650a1307", + "tokenHashHex": "55520a4f87639e29dee3eb478d12b351edefde44639189a151168ddcdfa156a1" + }, + { + "name": "customer", + "claims": { + "id": "55555555-5555-4555-8555-555555555555", + "kind": "client", + "issuedAt": 1792195200000, + "ttlSecs": 3600, + "orgId": "44444444-4444-4444-8444-444444444444", + "sessionName": "", + "ephemeralId": "browser", + "issuerKind": "project", + "issuerId": "22222222-2222-4222-8222-222222222222", + "permissionCeiling": "18446744073709551615", + "customer": { + "customerId": "66666666-6666-4666-8666-666666666666", + "sessionIds": [ + "77777777-7777-4777-8777-777777777777" + ], + "allowMask": 7 + }, + "testNumbersOnly": false + }, + "payloadHex": "01036d633155555555555545558555555555555555000001a14728840000000e1044444444444444448444444444444444000762726f777365720122222222222242228222222222222222ffffffffffffffff016666666666664666866666666666666601777777777777477787777777777777770007", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633155555555555545558555555555555555000001a14728840000000e1044444444444444448444444444444444000762726f777365720122222222222242228222222222222222ffffffffffffffff016666666666664666866666666666666601777777777777477787777777777777770007", + "signatureHex": "bd2e4c8303f04927cca6040c238913faeb09a6fe824c18179f5d4f6c724e5a51e76c1e16783b099a85214706b9144ef98928a45bd3ad55b054460f138890a50b", + "token": "pmfa_ct_AQNtYzFVVVVVVVVFVYVVVVVVVVVVAAABoUcohAAAAA4QRERERERERESERERERERERAAHYnJvd3NlcgEiIiIiIiJCIoIiIiIiIiIi__________8BZmZmZmZmRmaGZmZmZmZmZgF3d3d3d3dHd4d3d3d3d3d3AAe9LkyDA_BJJ8ymBAwjiRP66wmm_oJMGBefXU9sck5aUedsHhZ4OwmahSFHBrkUTvmJKKRb061VsFRGDxOIkKUL", + "tokenLength": 252, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633155555555555545558555555555555555000001a14728840000000e1044444444444444448444444444444444000762726f777365720122222222222242228222222222222222ffffffffffffffff016666666666664666866666666666666601777777777777477787777777777777770007bd2e4c8303f04927cca6040c238913faeb09a6fe824c18179f5d4f6c724e5a51e76c1e16783b099a85214706b9144ef98928a45bd3ad55b054460f138890a50b", + "tokenHashHex": "0f5aeca52b39e6321f1d98cf8729c10f9f5339ed31342dc525e4a58a198c72dd" + }, + { + "name": "customer-test-only", + "claims": { + "id": "55555555-5555-4555-8555-555555555555", + "kind": "client", + "issuedAt": 1792195200000, + "ttlSecs": 3600, + "orgId": "44444444-4444-4444-8444-444444444444", + "sessionName": "", + "ephemeralId": "browser", + "issuerKind": "project", + "issuerId": "22222222-2222-4222-8222-222222222222", + "permissionCeiling": "18446744073709551615", + "customer": { + "customerId": "66666666-6666-4666-8666-666666666666", + "sessionIds": [ + "77777777-7777-4777-8777-777777777777" + ], + "allowMask": 7 + }, + "testNumbersOnly": true + }, + "payloadHex": "01036d633155555555555545558555555555555555000001a14728840000000e1044444444444444448444444444444444000762726f777365720122222222222242228222222222222222ffffffffffffffff036666666666664666866666666666666601777777777777477787777777777777770007", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633155555555555545558555555555555555000001a14728840000000e1044444444444444448444444444444444000762726f777365720122222222222242228222222222222222ffffffffffffffff036666666666664666866666666666666601777777777777477787777777777777770007", + "signatureHex": "00bc981be2745c7b8339e2c7421ec6d5efcb3044257a5c626755f6ade8c936f92a500c3ebf6ec09cf0d889b66819c399b0a9ab78dfa082899618d88c0ad6170a", + "token": "pmfa_ct_AQNtYzFVVVVVVVVFVYVVVVVVVVVVAAABoUcohAAAAA4QRERERERERESERERERERERAAHYnJvd3NlcgEiIiIiIiJCIoIiIiIiIiIi__________8DZmZmZmZmRmaGZmZmZmZmZgF3d3d3d3dHd4d3d3d3d3d3AAcAvJgb4nRce4M54sdCHsbV78swRCV6XGJnVfat6Mk2-SpQDD6_bsCc8NiJtmgZw5mwqat436CCiZYY2IwK1hcK", + "tokenLength": 252, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633155555555555545558555555555555555000001a14728840000000e1044444444444444448444444444444444000762726f777365720122222222222242228222222222222222ffffffffffffffff03666666666666466686666666666666660177777777777747778777777777777777000700bc981be2745c7b8339e2c7421ec6d5efcb3044257a5c626755f6ade8c936f92a500c3ebf6ec09cf0d889b66819c399b0a9ab78dfa082899618d88c0ad6170a", + "tokenHashHex": "bbb27eea340d8119e42bf6ccc27d527bd001898abac562fdc309f38a64acb586" + }, + { + "name": "human", + "claims": { + "id": "88888888-8888-4888-8888-888888888888", + "kind": "client", + "issuedAt": 1792195200000, + "ttlSecs": 3600, + "orgId": "44444444-4444-4444-8444-444444444444", + "sessionName": "test-number", + "ephemeralId": "browser", + "issuerKind": "human", + "issuerId": "user-test", + "permissionCeiling": "18446744073709551615", + "userId": "user-test", + "sessionId": "session-test", + "region": "br", + "testNumbersOnly": false + }, + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff000c73657373696f6e2d74657374026272", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff000c73657373696f6e2d74657374026272", + "signatureHex": "b0867675c35f9931c4ee48935f880eecc70966bbdc942c02d98588d565bbc09076a397209a8e3377062699d010bb1dab6ccb31021258afd4abedba19887c5f02", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wAMc2Vzc2lvbi10ZXN0AmJysIZ2dcNfmTHE7kiTX4gO7McJZrvclCwC2YWI1WW7wJB2o5cgmo4zdwYmmdAQux2rbMsxAhJYr9Sr7boZiHxfAg", + "tokenLength": 234, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff000c73657373696f6e2d74657374026272b0867675c35f9931c4ee48935f880eecc70966bbdc942c02d98588d565bbc09076a397209a8e3377062699d010bb1dab6ccb31021258afd4abedba19887c5f02", + "tokenHashHex": "04bc4b1681f7464cdb9cc73c39bf1f85fe0699f03e480872b540078e5719e624" + }, + { + "name": "human-test-only", + "claims": { + "id": "88888888-8888-4888-8888-888888888888", + "kind": "client", + "issuedAt": 1792195200000, + "ttlSecs": 3600, + "orgId": "44444444-4444-4444-8444-444444444444", + "sessionName": "test-number", + "ephemeralId": "browser", + "issuerKind": "human", + "issuerId": "user-test", + "permissionCeiling": "18446744073709551615", + "userId": "user-test", + "sessionId": "session-test", + "region": "br", + "testNumbersOnly": true + }, + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff020c73657373696f6e2d74657374026272", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff020c73657373696f6e2d74657374026272", + "signatureHex": "cc9840f3dcacc013a31ad88e0fbe30b3d49ac9823e1e8b9e8ea0f4d95426a0c63eb0b50e7a4a65b3a49f1c21231802b0f8d338879232a22f45d44d3220e7810d", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wIMc2Vzc2lvbi10ZXN0AmJyzJhA89yswBOjGtiOD74ws9SayYI-HouejqD02VQmoMY-sLUOekpls6SfHCEjGAKw-NM4h5Iyoi9F1E0yIOeBDQ", + "tokenLength": 234, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff020c73657373696f6e2d74657374026272cc9840f3dcacc013a31ad88e0fbe30b3d49ac9823e1e8b9e8ea0f4d95426a0c63eb0b50e7a4a65b3a49f1c21231802b0f8d338879232a22f45d44d3220e7810d", + "tokenHashHex": "1c80f128a7dbc9fef87b4917eae8f14cef960b8c0d9a18c8f6da28e98f39f573" + }, + { + "name": "console-www", + "claims": { + "id": "88888888-8888-4888-8888-888888888888", + "kind": "client", + "issuedAt": 1792195200000, + "ttlSecs": 3600, + "orgId": "44444444-4444-4444-8444-444444444444", + "sessionName": "test-number", + "ephemeralId": "browser", + "issuerKind": "human", + "issuerId": "user-test", + "permissionCeiling": "18446744073709551615", + "userId": "user-test", + "sessionId": "session-test", + "region": "br", + "testNumbersOnly": false, + "console": { + "projectId": "22222222-2222-4222-8222-222222222222", + "sessionIds": [ + "77777777-7777-4777-8777-777777777777" + ], + "origin": "https://www.polymorfa.com" + } + }, + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signatureHex": "4261a093bf65fc5e340121a9ecfa87251faaa6d380d734777f87d88a5e2fb2700213943622205b15a85a9593a49f50ca37a410df7f7dd6f6d6a3a1c27c487c0c", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgF3d3d3d3dHd4d3d3d3d3d3GWh0dHBzOi8vd3d3LnBvbHltb3JmYS5jb21CYaCTv2X8XjQBIans-oclH6qm04DXNHd_h9iKXi-ycAITlDYiIFsVqFqVk6SfUMo3pBDff33W9tajocJ8SHwM", + "tokenLength": 312, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d4261a093bf65fc5e340121a9ecfa87251faaa6d380d734777f87d88a5e2fb2700213943622205b15a85a9593a49f50ca37a410df7f7dd6f6d6a3a1c27c487c0c", + "tokenHashHex": "fc0a354da098db2d4459889a3d615956d30adb9c6fc907bebb9ce89791540568" + }, + { + "name": "console-www-test-only", + "claims": { + "id": "88888888-8888-4888-8888-888888888888", + "kind": "client", + "issuedAt": 1792195200000, + "ttlSecs": 3600, + "orgId": "44444444-4444-4444-8444-444444444444", + "sessionName": "test-number", + "ephemeralId": "browser", + "issuerKind": "human", + "issuerId": "user-test", + "permissionCeiling": "18446744073709551615", + "userId": "user-test", + "sessionId": "session-test", + "region": "br", + "testNumbersOnly": true, + "console": { + "projectId": "22222222-2222-4222-8222-222222222222", + "sessionIds": [ + "77777777-7777-4777-8777-777777777777" + ], + "origin": "https://www.polymorfa.com" + } + }, + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff060c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff060c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signatureHex": "eb726490d835ac4fb9f9757d176b4a87368eff119b788ae23f0ede3ed41ffce6c85d4d49da8f369e3d02940bff3379f367206f1d62d2e1afb38efab3c7daa00a", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wYMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgF3d3d3d3dHd4d3d3d3d3d3GWh0dHBzOi8vd3d3LnBvbHltb3JmYS5jb23rcmSQ2DWsT7n5dX0Xa0qHNo7_EZt4iuI_Dt4-1B_85shdTUnajzaePQKUC_8zefNnIG8dYtLhr7OO-rPH2qAK", + "tokenLength": 312, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff060c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6deb726490d835ac4fb9f9757d176b4a87368eff119b788ae23f0ede3ed41ffce6c85d4d49da8f369e3d02940bff3379f367206f1d62d2e1afb38efab3c7daa00a", + "tokenHashHex": "8335b2391c91ef7cbf99eb9cc6762fc9fcd267724c4c46037d162b183c19c0ee" + }, + { + "name": "console-docs-all-blocks", + "claims": { + "id": "88888888-8888-4888-8888-888888888888", + "kind": "client", + "issuedAt": 1792195200000, + "ttlSecs": 3600, + "orgId": "44444444-4444-4444-8444-444444444444", + "sessionName": "", + "ephemeralId": "browser", + "issuerKind": "human", + "issuerId": "user-test", + "permissionCeiling": "18446744073709551615", + "userId": "user-test", + "sessionId": "session-test", + "region": "br", + "testNumbersOnly": false, + "customer": { + "customerId": "66666666-6666-4666-8666-666666666666", + "sessionIds": [ + "77777777-7777-4777-8777-777777777777" + ], + "allowMask": 7 + }, + "console": { + "projectId": "22222222-2222-4222-8222-222222222222", + "sessionIds": [ + "77777777-7777-4777-8777-777777777777" + ], + "origin": "https://docs.polymorfa.com" + } + }, + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e1044444444444444448444444444444444000762726f777365720209757365722d74657374ffffffffffffffff0566666666666646668666666666666666017777777777774777877777777777777700070c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771a68747470733a2f2f646f63732e706f6c796d6f7266612e636f6d", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e1044444444444444448444444444444444000762726f777365720209757365722d74657374ffffffffffffffff0566666666666646668666666666666666017777777777774777877777777777777700070c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771a68747470733a2f2f646f63732e706f6c796d6f7266612e636f6d", + "signatureHex": "4a441052fba43e67ec4b7a8357d4a5ce9ea3d381db745536158f0c71766ae26eba08c7e1d4886e312e60b983ffd8231fdea8a6de5b3689347f87fed1248f6805", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAAHYnJvd3NlcgIJdXNlci10ZXN0__________8FZmZmZmZmRmaGZmZmZmZmZgF3d3d3d3dHd4d3d3d3d3d3AAcMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgF3d3d3d3dHd4d3d3d3d3d3Gmh0dHBzOi8vZG9jcy5wb2x5bW9yZmEuY29tSkQQUvukPmfsS3qDV9Slzp6j04HbdFU2FY8McXZq4m66CMfh1IhuMS5guYP_2CMf3qim3ls2iTR_h_7RJI9oBQ", + "tokenLength": 346, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e1044444444444444448444444444444444000762726f777365720209757365722d74657374ffffffffffffffff0566666666666646668666666666666666017777777777774777877777777777777700070c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771a68747470733a2f2f646f63732e706f6c796d6f7266612e636f6d4a441052fba43e67ec4b7a8357d4a5ce9ea3d381db745536158f0c71766ae26eba08c7e1d4886e312e60b983ffd8231fdea8a6de5b3689347f87fed1248f6805", + "tokenHashHex": "21f27fd09e9eed68d910122e1c06f3a54a0c8018c6d7262f4ec89e2be1f94bfd" + }, + { + "name": "console-docs-all-blocks-test-only", + "claims": { + "id": "88888888-8888-4888-8888-888888888888", + "kind": "client", + "issuedAt": 1792195200000, + "ttlSecs": 3600, + "orgId": "44444444-4444-4444-8444-444444444444", + "sessionName": "", + "ephemeralId": "browser", + "issuerKind": "human", + "issuerId": "user-test", + "permissionCeiling": "18446744073709551615", + "userId": "user-test", + "sessionId": "session-test", + "region": "br", + "testNumbersOnly": true, + "customer": { + "customerId": "66666666-6666-4666-8666-666666666666", + "sessionIds": [ + "77777777-7777-4777-8777-777777777777" + ], + "allowMask": 7 + }, + "console": { + "projectId": "22222222-2222-4222-8222-222222222222", + "sessionIds": [ + "77777777-7777-4777-8777-777777777777" + ], + "origin": "https://docs.polymorfa.com" + } + }, + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e1044444444444444448444444444444444000762726f777365720209757365722d74657374ffffffffffffffff0766666666666646668666666666666666017777777777774777877777777777777700070c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771a68747470733a2f2f646f63732e706f6c796d6f7266612e636f6d", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e1044444444444444448444444444444444000762726f777365720209757365722d74657374ffffffffffffffff0766666666666646668666666666666666017777777777774777877777777777777700070c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771a68747470733a2f2f646f63732e706f6c796d6f7266612e636f6d", + "signatureHex": "8d06f148b2405d1b8d4e36d5f48fa1dd04bf1f8800c52f6bfeb3ae9ba78d240178b4a4bf2c40cb0d275173f7eef578854e6de5a26a4905bcd0e8e9996c8e8a0e", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAAHYnJvd3NlcgIJdXNlci10ZXN0__________8HZmZmZmZmRmaGZmZmZmZmZgF3d3d3d3dHd4d3d3d3d3d3AAcMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgF3d3d3d3dHd4d3d3d3d3d3Gmh0dHBzOi8vZG9jcy5wb2x5bW9yZmEuY29tjQbxSLJAXRuNTjbV9I-h3QS_H4gAxS9r_rOum6eNJAF4tKS_LEDLDSdRc_fu9XiFTm3lompJBbzQ6OmZbI6KDg", + "tokenLength": 346, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e1044444444444444448444444444444444000762726f777365720209757365722d74657374ffffffffffffffff0766666666666646668666666666666666017777777777774777877777777777777700070c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771a68747470733a2f2f646f63732e706f6c796d6f7266612e636f6d8d06f148b2405d1b8d4e36d5f48fa1dd04bf1f8800c52f6bfeb3ae9ba78d240178b4a4bf2c40cb0d275173f7eef578854e6de5a26a4905bcd0e8e9996c8e8a0e", + "tokenHashHex": "1c028f72d26f22febf7ac15087c6bcdcf508d534e753d55927eab7fd6951ccfc" + } + ], + "rejections": [ + { + "name": "console-count-0", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222200777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222200777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signatureHex": "1f79ff8699e78ae6644dd0d4fdbb080064028fb9cca75fdadbbd3f1133e145de46452b154995f6ca94831a6fb46602330965d5af8b46be22159a594e5f9dcb0c", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgB3d3d3d3dHd4d3d3d3d3d3GWh0dHBzOi8vd3d3LnBvbHltb3JmYS5jb20fef-GmeeK5mRN0NT9uwgAZAKPucynX9rbvT8RM-FF3kZFKxVJlfbKlIMab7RmAjMJZdWvi0a-IhWaWU5fncsM", + "tokenLength": 312, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222200777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d1f79ff8699e78ae6644dd0d4fdbb080064028fb9cca75fdadbbd3f1133e145de46452b154995f6ca94831a6fb46602330965d5af8b46be22159a594e5f9dcb0c", + "tokenHashHex": "2774cd35e2a8a89ab704f6d1facdd3370b7ac27f6d8714733d45717a51a42d65", + "expectedError": "invalid credential" + }, + { + "name": "console-count-65", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222241777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222241777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signatureHex": "db4d67537cfd9dd4337f788c3b8737324864950c6b35c1db2cffbda3f59b86475a58d1c7b6a8938d106595a67fa5224b16b993df3031976c1db15d0da3ec0a0c", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIkF3d3d3d3dHd4d3d3d3d3d3GWh0dHBzOi8vd3d3LnBvbHltb3JmYS5jb23bTWdTfP2d1DN_eIw7hzcySGSVDGs1wdss_72j9ZuGR1pY0ce2qJONEGWVpn-lIksWuZPfMDGXbB2xXQ2j7AoM", + "tokenLength": 312, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222241777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6ddb4d67537cfd9dd4337f788c3b8737324864950c6b35c1db2cffbda3f59b86475a58d1c7b6a8938d106595a67fa5224b16b993df3031976c1db15d0da3ec0a0c", + "tokenHashHex": "7fc5152457aa110d956187f119521a8372a2c5a3eb0a65f00c1ede1e002dfaf4", + "expectedError": "invalid credential" + }, + { + "name": "console-duplicate", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d74657374026272222222222222422282222222222222220277777777777747778777777777777777777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d74657374026272222222222222422282222222222222220277777777777747778777777777777777777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signatureHex": "45f4fcf4fb03089132977e9ed3775c7671f6065423b2e490f198690593b9edd3901664ddceb7daa455c150ed272f30ed65f67317b4f4c6c53582aa8b19a1af0b", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgJ3d3d3d3dHd4d3d3d3d3d3d3d3d3d3R3eHd3d3d3d3dxlodHRwczovL3d3dy5wb2x5bW9yZmEuY29tRfT89PsDCJEyl36e03dcdnH2BlQjsuSQ8ZhpBZO57dOQFmTdzrfapFXBUO0nLzDtZfZzF7T0xsU1gqqLGaGvCw", + "tokenLength": 334, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d74657374026272222222222222422282222222222222220277777777777747778777777777777777777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d45f4fcf4fb03089132977e9ed3775c7671f6065423b2e490f198690593b9edd3901664ddceb7daa455c150ed272f30ed65f67317b4f4c6c53582aa8b19a1af0b", + "tokenHashHex": "8a342e88bf10e6e0934184ddc44a59af81715e7a89be5a0da275c076097909ad", + "expectedError": "invalid credential" + }, + { + "name": "console-unsorted", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d74657374026272222222222222422282222222222222220288888888888848888888888888888888777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d74657374026272222222222222422282222222222222220288888888888848888888888888888888777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signatureHex": "1b9dd31b3175075c6bd62b0465a5c0f5c8ea116875d0961640b9ceed1d2f59db55bfbc86397c8606154e32aa60dd9f58d03a97e8b9cfd0c368b72374a31d2b0d", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgKIiIiIiIhIiIiIiIiIiIiId3d3d3d3R3eHd3d3d3d3dxlodHRwczovL3d3dy5wb2x5bW9yZmEuY29tG53TGzF1B1xr1isEZaXA9cjqEWh10JYWQLnO7R0vWdtVv7yGOXyGBhVOMqpg3Z9Y0DqX6LnP0MNotyN0ox0rDQ", + "tokenLength": 334, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d74657374026272222222222222422282222222222222220288888888888848888888888888888888777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d1b9dd31b3175075c6bd62b0465a5c0f5c8ea116875d0961640b9ceed1d2f59db55bfbc86397c8606154e32aa60dd9f58d03a97e8b9cfd0c368b72374a31d2b0d", + "tokenHashHex": "05325595cd10d34deb5f567fdec1df8e54985605910f2ed80d8397534e9f7e0d", + "expectedError": "invalid credential" + }, + { + "name": "console-origin-https://attacker.example", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771868747470733a2f2f61747461636b65722e6578616d706c65", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771868747470733a2f2f61747461636b65722e6578616d706c65", + "signatureHex": "d01f216ff2b4b6751a9290ae03af64c15a0403b6cee01fa3013b5e5f2ee8ebaed2ed05daa01f6c2e31c25ea1391a148a93cfa69f046a560842b18ccd1be1f401", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgF3d3d3d3dHd4d3d3d3d3d3GGh0dHBzOi8vYXR0YWNrZXIuZXhhbXBsZdAfIW_ytLZ1GpKQrgOvZMFaBAO2zuAfowE7Xl8u6Ouu0u0F2qAfbC4xwl6hORoUipPPpp8EalYIQrGMzRvh9AE", + "tokenLength": 311, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771868747470733a2f2f61747461636b65722e6578616d706c65d01f216ff2b4b6751a9290ae03af64c15a0403b6cee01fa3013b5e5f2ee8ebaed2ed05daa01f6c2e31c25ea1391a148a93cfa69f046a560842b18ccd1be1f401", + "tokenHashHex": "3595e0a875fc25aa193924b14eaf05365419732bf86f891af7b28a4e27616205", + "expectedError": "invalid credential" + }, + { + "name": "console-origin-https://www.polymorfa.com/", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771a68747470733a2f2f7777772e706f6c796d6f7266612e636f6d2f", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771a68747470733a2f2f7777772e706f6c796d6f7266612e636f6d2f", + "signatureHex": "caad7224806615ad2b7bbcb8a84411dbbdd9d1d77d6ea1926a0ed43a97b12b12d3b46ca630a0531af131b685c8d9a1ba7d0e001478aed2f8c2bfd93c184b9904", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgF3d3d3d3dHd4d3d3d3d3d3Gmh0dHBzOi8vd3d3LnBvbHltb3JmYS5jb20vyq1yJIBmFa0re7y4qEQR273Z0dd9bqGSag7UOpexKxLTtGymMKBTGvExtoXI2aG6fQ4AFHiu0vjCv9k8GEuZBA", + "tokenLength": 314, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771a68747470733a2f2f7777772e706f6c796d6f7266612e636f6d2fcaad7224806615ad2b7bbcb8a84411dbbdd9d1d77d6ea1926a0ed43a97b12b12d3b46ca630a0531af131b685c8d9a1ba7d0e001478aed2f8c2bfd93c184b9904", + "tokenHashHex": "878a52987e8403584b34f31fa5266188b643bee112a592ae21d0f4610d2c7787", + "expectedError": "invalid credential" + }, + { + "name": "console-origin-https://www.polymorfa.com.attacker.example", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777772a68747470733a2f2f7777772e706f6c796d6f7266612e636f6d2e61747461636b65722e6578616d706c65", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777772a68747470733a2f2f7777772e706f6c796d6f7266612e636f6d2e61747461636b65722e6578616d706c65", + "signatureHex": "333b1efcb02c44c019ec3447c89ad23b8bc7eb0674d588295daa37da59aad3641affdc76a96f8e225306b845a20763f9276f0944d8a4d81123531f3fd2e51400", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgF3d3d3d3dHd4d3d3d3d3d3Kmh0dHBzOi8vd3d3LnBvbHltb3JmYS5jb20uYXR0YWNrZXIuZXhhbXBsZTM7HvywLETAGew0R8ia0juLx-sGdNWIKV2qN9pZqtNkGv_cdqlvjiJTBrhFogdj-SdvCUTYpNgRI1MfP9LlFAA", + "tokenLength": 335, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777772a68747470733a2f2f7777772e706f6c796d6f7266612e636f6d2e61747461636b65722e6578616d706c65333b1efcb02c44c019ec3447c89ad23b8bc7eb0674d588295daa37da59aad3641affdc76a96f8e225306b845a20763f9276f0944d8a4d81123531f3fd2e51400", + "tokenHashHex": "0c77d7ec6d79df1abebd939565a4032640f1b2153d08ccec9950fc093f5e30cb", + "expectedError": "invalid credential" + }, + { + "name": "console-nil-project", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262720000000000000000000000000000000001777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262720000000000000000000000000000000001777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d", + "signatureHex": "9e0f685a33f24c0baa9debcf0c26a6bca09d8ad4337e831c761bd3e832ba916a81db22a478acf4a301a79d977a06baf707a95ca3ac476b5b16fabee19a9ad90f", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyAAAAAAAAAAAAAAAAAAAAAAF3d3d3d3dHd4d3d3d3d3d3GWh0dHBzOi8vd3d3LnBvbHltb3JmYS5jb22eD2haM_JMC6qd688MJqa8oJ2K1DN-gxx2G9PoMrqRaoHbIqR4rPSjAaedl3oGuvcHqVyjrEdrWxb6vuGamtkP", + "tokenLength": 312, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262720000000000000000000000000000000001777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d9e0f685a33f24c0baa9debcf0c26a6bca09d8ad4337e831c761bd3e832ba916a81db22a478acf4a301a79d977a06baf707a95ca3ac476b5b16fabee19a9ad90f", + "tokenHashHex": "04f462708a86aede99364862e9b88e8d12e21f645db989447e639f893f0873e9", + "expectedError": "invalid credential" + }, + { + "name": "console-truncated", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f", + "signatureHex": "7b2093c293eb5821490a4014f80e91c1a05ba16a6085345f8a10a07925c59c4a020848865a9da8724220e1570ae317c11f978b1ef8cc21979450931e8f64430e", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgF3d3d3d3dHd4d3d3d3d3d3GWh0dHBzOi8vd3d3LnBvbHltb3JmYS5jb3sgk8KT61ghSQpAFPgOkcGgW6FqYIU0X4oQoHklxZxKAghIhlqdqHJCIOFXCuMXwR-Xix74zCGXlFCTHo9kQw4", + "tokenLength": 311, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f7b2093c293eb5821490a4014f80e91c1a05ba16a6085345f8a10a07925c59c4a020848865a9da8724220e1570ae317c11f978b1ef8cc21979450931e8f64430e", + "tokenHashHex": "89622c7897d7cf30c4a6881278fecc484dba8e546485f36e812cee127022022f", + "expectedError": "invalid credential" + }, + { + "name": "console-trailing", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d00", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d00", + "signatureHex": "e703ca91e8a33515d7ec3dcff0fe4c851a2d52ac107b928217afcd8279e2e382d0c32a6a2da8017f7e12fbb7e427c8efb344330b245820bd563bd2d56f122609", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wQMc2Vzc2lvbi10ZXN0AmJyIiIiIiIiQiKCIiIiIiIiIgF3d3d3d3dHd4d3d3d3d3d3GWh0dHBzOi8vd3d3LnBvbHltb3JmYS5jb20A5wPKkeijNRXX7D3P8P5MhRotUqwQe5KCF6_Ngnni44LQwypqLagBf34S-7fkJ8jvs0QzCyRYIL1WO9LVbxImCQ", + "tokenLength": 314, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff040c73657373696f6e2d746573740262722222222222224222822222222222222201777777777777477787777777777777771968747470733a2f2f7777772e706f6c796d6f7266612e636f6d00e703ca91e8a33515d7ec3dcff0fe4c851a2d52ac107b928217afcd8279e2e382d0c32a6a2da8017f7e12fbb7e427c8efb344330b245820bd563bd2d56f122609", + "tokenHashHex": "890e6f274f2aac3dc264ab06aec9d47ca29e75c3b1c9f9cc8f3dc492c77cf0b5", + "expectedError": "invalid credential" + }, + { + "name": "reserved-bit3", + "payloadHex": "01036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff080c73657373696f6e2d74657374026272", + "signedInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e456432353531392e636c69656e7400746573740068747470733a2f2f706c6174666f726d2e6578616d706c652e7465737400706f6c796d6f7266612d6170690001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff080c73657373696f6e2d74657374026272", + "signatureHex": "d0bfcd42444d91414a2c554e74e71ec1967218817a6890a7d5b08f3e16a8bb8f17f13392b415350de50995fb078720423fcb8289398c40f060b400354bb7c209", + "token": "pmfa_ct_AQNtYzGIiIiIiIhIiIiIiIiIiIiIAAABoUcohAAAAA4QRERERERERESERERERERERAt0ZXN0LW51bWJlcgdicm93c2VyAgl1c2VyLXRlc3T__________wgMc2Vzc2lvbi10ZXN0AmJy0L_NQkRNkUFKLFVOdOcewZZyGIF6aJCn1bCPPhaou48X8TOStBU1DeUJlfsHhyBCP8uCiTmMQPBgtAA1S7fCCQ", + "tokenLength": 234, + "fingerprintInputHex": "706f6c796d6f7266612e6d616368696e652d63726564656e7469616c732e76312e686173682e636c69656e740001036d633188888888888848888888888888888888000001a14728840000000e10444444444444444484444444444444440b746573742d6e756d6265720762726f777365720209757365722d74657374ffffffffffffffff080c73657373696f6e2d74657374026272d0bfcd42444d91414a2c554e74e71ec1967218817a6890a7d5b08f3e16a8bb8f17f13392b415350de50995fb078720423fcb8289398c40f060b400354bb7c209", + "tokenHashHex": "88ece95bd41718f55c0a73337dbd82077e48eb7cec9e453cc0f13cf08c941d29", + "expectedError": "invalid credential" + } + ] + }, + "negative": [ + { + "name": "test-client-against-live-number", + "operation": "authorize-number", + "input": { + "clientCase": "project-issued-test-only", + "numberTestMode": false + }, + "expectedError": "forbidden" + }, + { + "name": "test-client-unknown-number-mode", + "operation": "authorize-number", + "input": { + "clientCase": "project-issued-test-only", + "numberTestMode": null + }, + "expectedError": "forbidden" + } + ], + "consoleOrigins": [ + "https://www.polymorfa.com", + "https://docs.polymorfa.com" + ] +} diff --git a/contracts/native-calls-simultaneous-calls.json b/contracts/native-calls-simultaneous-calls.json new file mode 100644 index 00000000..0b61f0b9 --- /dev/null +++ b/contracts/native-calls-simultaneous-calls.json @@ -0,0 +1,72 @@ +{ + "schemaVersion": 1, + "sourceCommit": "c1a43a1eb409ad57c2b9677c606a3d42d7b5598c", + "pin": "contracts/calls-simultaneous-calls.json", + "baselineUnchanged": true, + "published": false, + "claim": "Scoped source attribution and synthetic native wire fixtures. Execution, physical package qualification and publication are separate gates.", + "operations": [ + { + "operationId": "getCallSettings", + "method": "GET", + "path": "/platform/sessions/{session}/call-settings", + "languages": { + "typescript": { + "method": "MessagingClient.voip.retrieveCallSettings", + "test": "test/voip.test.ts" + }, + "go": { + "method": "MessagingClient.VoIP.RetrieveCallSettings", + "test": "voip_test.go" + }, + "python": { + "method": "AsyncMessagingClient.voip.retrieve_call_settings", + "test": "tests/test_voip.py" + }, + "php": { + "method": "MessagingClient.voip.retrieveCallSettings", + "test": "tests/voip.php" + }, + "rust": { + "method": "MessagingClient.voip.retrieve_call_settings", + "test": "tests/voip.rs" + }, + "dotnet": { + "method": "MessagingClient.Calls.RetrieveCallSettingsAsync", + "test": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + } + } + }, + { + "operationId": "updateCallSettings", + "method": "PUT", + "path": "/platform/sessions/{session}/call-settings", + "languages": { + "typescript": { + "method": "MessagingClient.voip.updateCallSettings", + "test": "test/voip.test.ts" + }, + "go": { + "method": "MessagingClient.VoIP.UpdateCallSettings", + "test": "voip_test.go" + }, + "python": { + "method": "AsyncMessagingClient.voip.update_call_settings", + "test": "tests/test_voip.py" + }, + "php": { + "method": "MessagingClient.voip.updateCallSettings", + "test": "tests/voip.php" + }, + "rust": { + "method": "MessagingClient.voip.update_call_settings", + "test": "tests/voip.rs" + }, + "dotnet": { + "method": "MessagingClient.Calls.UpdateCallSettingsAsync", + "test": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + } + } + } + ] +} diff --git a/contracts/native-campaign-supplements.json b/contracts/native-campaign-supplements.json new file mode 100644 index 00000000..ff10c036 --- /dev/null +++ b/contracts/native-campaign-supplements.json @@ -0,0 +1,1088 @@ +{ + "schemaVersion": 1, + "implementationReference": { + "repository": "polymorfa/sdks", + "typescriptCommit": "e24cb176fc1124f5fe852ff6c77902648fd13b4a", + "note": "TypeScript implementation the native ports mirror. It is not API acceptance, deployed availability or registry publication." + }, + "baselineLedger": { + "path": "contracts/coverage.json", + "operations": 579, + "note": "These supplement operations are outside the pinned 579-operation ledger. Neither file establishes equality with any later API operation inventory." + }, + "supplements": [ + { + "path": "contracts/campaign-frequency-cap.json", + "commit": "9bb1e2ed92d8b4a07ff727f8e7ae185498f0868f", + "sources": { + "messaging": "6d5b5477a86a85504f921933bf7b26f2c47506e21380fe73c8a5ff1002163b28", + "platform": "bf4c6056b75e2fb0293b48edc5d8c12c36cdcbd194b9d11159f59522d30badc3" + } + }, + { + "path": "contracts/campaign-segments-drip.json", + "commit": "9bb1e2ed92d8b4a07ff727f8e7ae185498f0868f", + "sources": { + "messaging": "6d5b5477a86a85504f921933bf7b26f2c47506e21380fe73c8a5ff1002163b28", + "platform": "bf4c6056b75e2fb0293b48edc5d8c12c36cdcbd194b9d11159f59522d30badc3" + } + } + ], + "operations": [ + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "platform", + "method": "GET", + "path": "/platform/campaign-settings/frequency-cap", + "operationId": "getCampaignFrequencyCap", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "Client.campaigns.getFrequencyCap", + "testFile": "test/campaign-frequency-cap.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns().GetFrequencyCap", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncClient.campaigns.get_frequency_cap", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "Client::campaigns.getFrequencyCap", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "OrganizationClient.campaigns().get_frequency_cap", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.GetFrequencyCapAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "platform", + "method": "PUT", + "path": "/platform/campaign-settings/frequency-cap", + "operationId": "updateCampaignFrequencyCap", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "Client.campaigns.updateFrequencyCap", + "testFile": "test/campaign-frequency-cap.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns().UpdateFrequencyCap", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncClient.campaigns.update_frequency_cap", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "Client::campaigns.updateFrequencyCap", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "OrganizationClient.campaigns().update_frequency_cap", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.UpdateFrequencyCapAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/enrollments", + "operationId": "enrollCampaignContact", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "Client.campaigns.enroll", + "testFile": "test/campaign-segments-drip.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns().Enroll", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncClient.campaigns.enroll", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "Client::campaigns.enroll", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "OrganizationClient.campaigns().enroll", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.EnrollAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}/enrollments", + "operationId": "enrollProjectCampaignContact", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "MessagingClient.campaigns.enroll", + "testFile": "test/campaign-segments-drip.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns().Enroll", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncMessagingClient.campaigns.enroll", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "MessagingClient::campaigns.enroll", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "MessagingClient.campaigns().enroll", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.EnrollAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "method": "GET", + "path": "/platform/audience-segments", + "operationId": "listAudienceSegments", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "Client.audienceSegments.list", + "testFile": "test/campaign-segments-drip.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments().List", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncClient.audience_segments.list", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "Client::audienceSegments.list", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "OrganizationClient.audience_segments().list", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "method": "POST", + "path": "/platform/audience-segments", + "operationId": "createAudienceSegment", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "Client.audienceSegments.create", + "testFile": "test/campaign-segments-drip.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments().Create", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncClient.audience_segments.create", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "Client::audienceSegments.create", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "OrganizationClient.audience_segments().create", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "method": "GET", + "path": "/platform/audience-segments/{segmentId}", + "operationId": "getAudienceSegment", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "Client.audienceSegments.retrieve", + "testFile": "test/campaign-segments-drip.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments().Retrieve", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncClient.audience_segments.retrieve", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "Client::audienceSegments.retrieve", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "OrganizationClient.audience_segments().retrieve", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "method": "PATCH", + "path": "/platform/audience-segments/{segmentId}", + "operationId": "updateAudienceSegment", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "Client.audienceSegments.update", + "testFile": "test/campaign-segments-drip.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments().Update", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncClient.audience_segments.update", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "Client::audienceSegments.update", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "OrganizationClient.audience_segments().update", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "method": "DELETE", + "path": "/platform/audience-segments/{segmentId}", + "operationId": "deleteAudienceSegment", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "Client.audienceSegments.delete", + "testFile": "test/campaign-segments-drip.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments().Delete", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncClient.audience_segments.delete", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "Client::audienceSegments.delete", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "OrganizationClient.audience_segments().delete", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "method": "GET", + "path": "/platform/audience-segments/{segmentId}/preview", + "operationId": "previewAudienceSegment", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "Client.audienceSegments.preview", + "testFile": "test/campaign-segments-drip.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments().Preview", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncClient.audience_segments.preview", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "Client::audienceSegments.preview", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "OrganizationClient.audience_segments().preview", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments.PreviewAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "method": "POST", + "path": "/platform/audience-segments/preview", + "operationId": "previewAudienceSegmentRules", + "languages": { + "typescript": { + "status": "covered", + "sdkMethod": "Client.audienceSegments.previewRules", + "testFile": "test/campaign-segments-drip.test.ts" + }, + "go": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments().PreviewRules", + "testFile": "campaign_segments_drip_test.go" + }, + "python": { + "status": "covered", + "sdkMethod": "AsyncClient.audience_segments.preview_rules", + "testFile": "tests/test_campaign_segments_drip.py" + }, + "php": { + "status": "covered", + "sdkMethod": "Client::audienceSegments.previewRules", + "testFile": "tests/campaign_segments_drip.php" + }, + "rust": { + "status": "covered", + "sdkMethod": "OrganizationClient.audience_segments().preview_rules", + "testFile": "tests/campaign_segments_drip.rs" + }, + "dotnet": { + "status": "covered", + "sdkMethod": "OrganizationClient.AudienceSegments.PreviewRulesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignSegmentsDripTests.cs" + } + } + } + ], + "fields": [ + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "platform", + "contractName": "CampaignRecipientFailureReason", + "shape": "enum value frequency_cap", + "native": { + "go": "CampaignFailureFrequencyCap", + "python": "frequency_cap", + "php": "frequency_cap", + "rust": "FrequencyCap", + "dotnet": "FrequencyCap" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "platform", + "contractName": "CampaignFrequencyCapSettings", + "shape": "schema", + "native": { + "go": "CampaignFrequencyCapSettings", + "python": "FrequencyCapSettings", + "php": "FrequencyCapSettings", + "rust": "CampaignFrequencyCapSettings", + "dotnet": "CampaignFrequencyCapSettings" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "platform", + "contractName": "UpdateCampaignFrequencyCapInput", + "shape": "schema", + "native": { + "go": "UpdateCampaignFrequencyCapInput", + "python": "UpdateFrequencyCap", + "php": "FrequencyCapInput", + "rust": "UpdateCampaignFrequencyCapRequest", + "dotnet": "UpdateCampaignFrequencyCapRequest" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "platform", + "contractName": "PlatformCampaign.frequencyCap", + "shape": "read", + "native": { + "go": "FrequencyCap", + "python": "frequencyCap", + "php": "frequencyCap", + "rust": "frequency_cap", + "dotnet": "FrequencyCap" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "platform", + "contractName": "CreatePlatformCampaignRequest.frequencyCap", + "shape": "write", + "native": { + "go": "FrequencyCap", + "python": "frequencyCap", + "php": "frequencyCap", + "rust": "frequency_cap", + "dotnet": "FrequencyCap" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "platform", + "contractName": "UpdatePlatformCampaignRequest.frequencyCap", + "shape": "write nullable", + "native": { + "go": "FrequencyCap", + "python": "frequencyCap", + "php": "frequencyCap", + "rust": "frequency_cap", + "dotnet": "FrequencyCap" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "platform", + "contractName": "PlatformCampaignAnalytics.frequencyCap", + "shape": "read", + "native": { + "go": "SkippedRecipientCount", + "python": "skippedRecipientCount", + "php": "skippedRecipientCount", + "rust": "skipped_recipient_count", + "dotnet": "SkippedRecipientCount" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "platform", + "contractName": "CampaignRecipient.failureReason", + "shape": "read", + "native": { + "go": "CampaignFailureFrequencyCap", + "python": "frequency_cap", + "php": "frequency_cap", + "rust": "FrequencyCap", + "dotnet": "FrequencyCap" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "messaging", + "contractName": "Campaign.frequencyCap", + "shape": "read", + "native": { + "go": "FrequencyCap", + "python": "frequencyCap", + "php": "frequencyCap", + "rust": "frequency_cap", + "dotnet": "FrequencyCap" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "messaging", + "contractName": "CampaignAnalytics.frequencyCap", + "shape": "read", + "native": { + "go": "SkippedRecipientCount", + "python": "skippedRecipientCount", + "php": "skippedRecipientCount", + "rust": "skipped_recipient_count", + "dotnet": "SkippedRecipientCount" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "messaging", + "contractName": "CreateCampaignRequest.frequencyCap", + "shape": "write", + "native": { + "go": "FrequencyCap", + "python": "frequencyCap", + "php": "frequencyCap", + "rust": "frequency_cap", + "dotnet": "FrequencyCap" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "messaging", + "contractName": "UpdateCampaignRequest.frequencyCap", + "shape": "write nullable", + "native": { + "go": "FrequencyCap", + "python": "frequencyCap", + "php": "frequencyCap", + "rust": "frequency_cap", + "dotnet": "FrequencyCap" + } + }, + { + "supplement": "contracts/campaign-frequency-cap.json", + "family": "messaging", + "contractName": "CampaignRecipient.failureReason", + "shape": "read", + "native": { + "go": "CampaignFailureFrequencyCap", + "python": "frequency_cap", + "php": "frequency_cap", + "rust": "FrequencyCap", + "dotnet": "FrequencyCap" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "AudienceSegment", + "shape": "schema", + "native": { + "go": "AudienceSegment", + "python": "AudienceSegment", + "php": "Segment", + "rust": "AudienceSegment", + "dotnet": "AudienceSegment" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "AudienceSegmentCondition", + "shape": "schema", + "native": { + "go": "AudienceSegmentCondition", + "python": "AudienceSegmentCondition", + "php": "Condition", + "rust": "AudienceSegmentCondition", + "dotnet": "AudienceSegmentCondition" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "AudienceSegmentPreview", + "shape": "schema", + "native": { + "go": "AudienceSegmentPreview", + "python": "AudienceSegmentPreview", + "php": "Preview", + "rust": "AudienceSegmentPreview", + "dotnet": "AudienceSegmentPreview" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "AudienceSegmentRules", + "shape": "schema", + "native": { + "go": "AudienceSegmentRules", + "python": "AudienceSegmentRules", + "php": "Rules", + "rust": "AudienceSegmentRules", + "dotnet": "AudienceSegmentRules" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CampaignDrip", + "shape": "schema", + "native": { + "go": "CampaignDrip", + "python": "Drip", + "php": "Drip", + "rust": "CampaignDrip", + "dotnet": "CampaignDrip" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CampaignDripInput", + "shape": "schema", + "native": { + "go": "CampaignDripInput", + "python": "DripInput", + "php": "DripInput", + "rust": "CampaignDripInput", + "dotnet": "CampaignDripInput" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CampaignDripPatch", + "shape": "schema", + "native": { + "go": "CampaignDripPatch", + "python": "DripPatch", + "php": "DripInput", + "rust": "CampaignDripPatch", + "dotnet": "CampaignDripPatch" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CampaignDripStats", + "shape": "schema", + "native": { + "go": "CampaignDripStats", + "python": "DripStats", + "php": "DripStats", + "rust": "CampaignDripStats", + "dotnet": "CampaignDripStats" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CampaignEnrollment", + "shape": "schema", + "native": { + "go": "CampaignEnrollment", + "python": "Enrollment", + "php": "Enrollment", + "rust": "CampaignEnrollment", + "dotnet": "CampaignEnrollment" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CampaignSendWindowRange", + "shape": "schema", + "native": { + "go": "CampaignSendWindowRange", + "python": "Hours", + "php": "StepWindow", + "rust": "CampaignSendWindowRange", + "dotnet": "CampaignSendWindowRange" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "EnrollCampaignContactRequest", + "shape": "schema", + "native": { + "go": "EnrollPlatformCampaignContactRequest", + "python": "EnrollPlatformContact", + "php": "PlatformEnrollInput", + "rust": "EnrollPlatformCampaignContactRequest", + "dotnet": "EnrollPlatformCampaignContactRequest" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "CampaignDripStepStats", + "shape": "schema", + "native": { + "go": "CampaignDripStepStats", + "python": "DripStepStats", + "php": "DripStepStats", + "rust": "CampaignDripStepStats", + "dotnet": "CampaignDripStepStats" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "EnrollCampaignContactRequest", + "shape": "schema", + "native": { + "go": "EnrollCampaignContactRequest", + "python": "EnrollContact", + "php": "EnrollInput", + "rust": "EnrollCampaignContactRequest", + "dotnet": "EnrollCampaignContactRequest" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "EnrollCampaignContactResponse", + "shape": "schema", + "native": { + "go": "CampaignEnrollment", + "python": "Enrollment", + "php": "Enrollment", + "rust": "CampaignEnrollment", + "dotnet": "CampaignEnrollment" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CreatePlatformCampaignRequest.segmentId", + "shape": "write omission only", + "native": { + "go": "SegmentID", + "python": "segmentId", + "php": "segmentId", + "rust": "segment_id", + "dotnet": "SegmentId" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CreatePlatformCampaignRequest.drip", + "shape": "write nullable", + "native": { + "go": "Drip", + "python": "drip", + "php": "drip", + "rust": "drip", + "dotnet": "Drip" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CampaignMessage.delayUnit", + "shape": "write", + "native": { + "go": "DelayUnit", + "python": "delayUnit", + "php": "delayUnit", + "rust": "delay_unit", + "dotnet": "DelayUnit" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CampaignMessage.sendWindow", + "shape": "write nullable", + "native": { + "go": "SendWindow", + "python": "sendWindow", + "php": "sendWindow", + "rust": "send_window", + "dotnet": "SendWindow" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "CampaignMessage.delayAfterSec", + "shape": "write bound 86400 or 2592000", + "native": { + "go": "DelayAfterSec", + "python": "delayAfterSec", + "php": "delayAfterSec", + "rust": "delay_after_sec", + "dotnet": "DelayAfterSec" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "PlatformCampaignAnalytics.drip", + "shape": "read nullable", + "native": { + "go": "Drip", + "python": "drip", + "php": "drip", + "rust": "drip", + "dotnet": "Drip" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "UpdatePlatformCampaignRequest.drip", + "shape": "write partial nullable", + "native": { + "go": "Drip", + "python": "drip", + "php": "drip", + "rust": "drip", + "dotnet": "Drip" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "UpdatePlatformCampaignRequest.segmentId", + "shape": "write nullable", + "native": { + "go": "SegmentID", + "python": "segmentId", + "php": "segmentId", + "rust": "segment_id", + "dotnet": "SegmentId" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "PlatformCampaign.drip", + "shape": "read nullable", + "native": { + "go": "Drip", + "python": "drip", + "php": "drip", + "rust": "drip", + "dotnet": "Drip" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "PlatformCampaign.enrollmentOpen", + "shape": "read nullable", + "native": { + "go": "EnrollmentOpen", + "python": "enrollmentOpen", + "php": "enrollmentOpen", + "rust": "enrollment_open", + "dotnet": "EnrollmentOpen" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "PlatformCampaign.pauseReason", + "shape": "read nullable", + "native": { + "go": "PauseReason", + "python": "pauseReason", + "php": "pauseReason", + "rust": "pause_reason", + "dotnet": "PauseReason" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "PlatformCampaign.segmentId", + "shape": "read nullable", + "native": { + "go": "SegmentID", + "python": "segmentId", + "php": "segmentId", + "rust": "segment_id", + "dotnet": "SegmentId" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "platform", + "contractName": "PlatformCampaign.segmentSnapshot", + "shape": "read nullable", + "native": { + "go": "SegmentSnapshot", + "python": "segmentSnapshot", + "php": "segmentSnapshot", + "rust": "segment_snapshot", + "dotnet": "SegmentSnapshot" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "Campaign.drip", + "shape": "read nullable", + "native": { + "go": "Drip", + "python": "drip", + "php": "drip", + "rust": "drip", + "dotnet": "Drip" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "Campaign.enrollmentOpen", + "shape": "read nullable", + "native": { + "go": "EnrollmentOpen", + "python": "enrollmentOpen", + "php": "enrollmentOpen", + "rust": "enrollment_open", + "dotnet": "EnrollmentOpen" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "Campaign.pauseReason", + "shape": "read nullable", + "native": { + "go": "PauseReason", + "python": "pauseReason", + "php": "pauseReason", + "rust": "pause_reason", + "dotnet": "PauseReason" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "Campaign.segmentId", + "shape": "read only, nullable", + "native": { + "go": "SegmentID", + "python": "segmentId", + "php": "segmentId", + "rust": "segment_id", + "dotnet": "SegmentId" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "Campaign.segmentSnapshot", + "shape": "read nullable", + "native": { + "go": "SegmentSnapshot", + "python": "segmentSnapshot", + "php": "segmentSnapshot", + "rust": "segment_snapshot", + "dotnet": "SegmentSnapshot" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "CampaignAnalytics.drip", + "shape": "read nullable", + "native": { + "go": "Drip", + "python": "drip", + "php": "drip", + "rust": "drip", + "dotnet": "Drip" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "CreateCampaignRequest.drip", + "shape": "write nullable", + "native": { + "go": "Drip", + "python": "drip", + "php": "drip", + "rust": "drip", + "dotnet": "Drip" + } + }, + { + "supplement": "contracts/campaign-segments-drip.json", + "family": "messaging", + "contractName": "UpdateCampaignRequest.drip", + "shape": "write partial nullable", + "native": { + "go": "Drip", + "python": "drip", + "php": "drip", + "rust": "drip", + "dotnet": "Drip" + } + } + ] +} diff --git a/contracts/native-official-round2.json b/contracts/native-official-round2.json new file mode 100644 index 00000000..73cb97b6 --- /dev/null +++ b/contracts/native-official-round2.json @@ -0,0 +1,557 @@ +{ + "schemaVersion": 1, + "sourceCommit": "9bb1e2ed92d8b4a07ff727f8e7ae185498f0868f", + "pin": "contracts/official-api-round2.json", + "typescriptHandoff": "f9937116b17f9962de8a3eb632c44d0399b05002", + "claim": "Native source and synthetic wire fixtures; exact-head execution and package qualification are separate gates.", + "baselineUnchanged": true, + "published": false, + "operations": [ + { + "operationId": "listCloudCatalogs", + "method": "GET", + "path": "/messaging/{session}/catalogs", + "snapshot": "contracts/official-api-round2/oa-16.json", + "languages": { + "typescript": { + "method": "MessagingClient.catalogs.list", + "test": "test/official-api-round2.test.ts" + }, + "go": { + "method": "MessagingClient.Catalogs.List", + "test": "official_round2_test.go" + }, + "python": { + "method": "AsyncMessagingClient.catalogs.list", + "test": "tests/test_official_round2.py" + }, + "php": { + "method": "MessagingClient.catalogs.list", + "test": "tests/official_round2.php" + }, + "rust": { + "method": "MessagingClient.catalogs.list", + "test": "tests/official_round2.rs" + }, + "dotnet": { + "method": "MessagingClient.Catalogs.ListAsync", + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + { + "operationId": "getCloudCatalog", + "method": "GET", + "path": "/messaging/{session}/catalogs/{catalogId}", + "snapshot": "contracts/official-api-round2/oa-16.json", + "languages": { + "typescript": { + "method": "MessagingClient.catalogs.retrieve", + "test": "test/official-api-round2.test.ts" + }, + "go": { + "method": "MessagingClient.Catalogs.Retrieve", + "test": "official_round2_test.go" + }, + "python": { + "method": "AsyncMessagingClient.catalogs.retrieve", + "test": "tests/test_official_round2.py" + }, + "php": { + "method": "MessagingClient.catalogs.retrieve", + "test": "tests/official_round2.php" + }, + "rust": { + "method": "MessagingClient.catalogs.retrieve", + "test": "tests/official_round2.rs" + }, + "dotnet": { + "method": "MessagingClient.Catalogs.RetrieveAsync", + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + { + "operationId": "syncCloudCatalog", + "method": "POST", + "path": "/messaging/{session}/catalogs/{catalogId}/sync", + "snapshot": "contracts/official-api-round2/oa-16.json", + "languages": { + "typescript": { + "method": "MessagingClient.catalogs.sync", + "test": "test/official-api-round2.test.ts" + }, + "go": { + "method": "MessagingClient.Catalogs.Sync", + "test": "official_round2_test.go" + }, + "python": { + "method": "AsyncMessagingClient.catalogs.sync", + "test": "tests/test_official_round2.py" + }, + "php": { + "method": "MessagingClient.catalogs.sync", + "test": "tests/official_round2.php" + }, + "rust": { + "method": "MessagingClient.catalogs.sync", + "test": "tests/official_round2.rs" + }, + "dotnet": { + "method": "MessagingClient.Catalogs.SyncAsync", + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + { + "operationId": "listCloudCatalogProducts", + "method": "GET", + "path": "/messaging/{session}/catalogs/{catalogId}/products", + "snapshot": "contracts/official-api-round2/oa-16.json", + "languages": { + "typescript": { + "method": "MessagingClient.catalogs.listProducts", + "test": "test/official-api-round2.test.ts" + }, + "go": { + "method": "MessagingClient.Catalogs.ListProducts", + "test": "official_round2_test.go" + }, + "python": { + "method": "AsyncMessagingClient.catalogs.list_products", + "test": "tests/test_official_round2.py" + }, + "php": { + "method": "MessagingClient.catalogs.listProducts", + "test": "tests/official_round2.php" + }, + "rust": { + "method": "MessagingClient.catalogs.list_products", + "test": "tests/official_round2.rs" + }, + "dotnet": { + "method": "MessagingClient.Catalogs.ListProductsAsync", + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + { + "operationId": "getMarketingMessagesStatus", + "method": "GET", + "path": "/messaging/{session}/marketing-messages/status", + "snapshot": "contracts/official-api-round2/oa-17.json", + "languages": { + "typescript": { + "method": "MessagingClient.cloudMarketing.getStatus", + "test": "test/official-api-round2.test.ts" + }, + "go": { + "method": "MessagingClient.CloudMarketing.GetStatus", + "test": "official_round2_test.go" + }, + "python": { + "method": "AsyncMessagingClient.cloud_marketing.get_status", + "test": "tests/test_official_round2.py" + }, + "php": { + "method": "MessagingClient.cloudMarketing.getStatus", + "test": "tests/official_round2.php" + }, + "rust": { + "method": "MessagingClient.cloud_marketing.get_status", + "test": "tests/official_round2.rs" + }, + "dotnet": { + "method": "MessagingClient.CloudGraph.GetMarketingMessagesStatusAsync", + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + { + "operationId": "setOfficialGroupPicture", + "method": "PUT", + "path": "/messaging/{session}/official-groups/{group}/picture", + "snapshot": "contracts/official-api-round2/oa-21.json", + "languages": { + "typescript": { + "method": "MessagingClient.officialGroups.setPicture", + "test": "test/official-api-round2.test.ts" + }, + "go": { + "method": "MessagingClient.OfficialGroups.SetPicture", + "test": "official_round2_test.go" + }, + "python": { + "method": "AsyncMessagingClient.official_groups.set_picture", + "test": "tests/test_official_round2.py" + }, + "php": { + "method": "MessagingClient.officialGroups.setPicture", + "test": "tests/official_round2.php" + }, + "rust": { + "method": "MessagingClient.official_groups.set_picture", + "test": "tests/official_round2.rs" + }, + "dotnet": { + "method": "MessagingClient.OfficialGroups.SetPictureAsync", + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + } + ], + "fields": { + "parentBsuid": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "ContactPreviousIdentity": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "ContactIdentityChange": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "CloudOrder": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "CloudOrderItem": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "marketing.onboarding_updated": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "contact.marketing_preference_updated": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "QuickLinkStatus.errorCode": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "GraphMarketing.owner_business_info": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "GraphMarketing.disable_marketing_messages_on_cloud_api": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "WebhookBatchingInput": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "WebhookEventBatch": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + }, + "wholeBodySignature": { + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + } + } + }, + "verification": { + "typescript": { + "sourceFile": "src/messaging/catalogs.ts", + "wireTest": "test/official-api-round2.test.ts", + "requiredGate": "Exact combined-head native CI and physical package qualification." + }, + "go": { + "sourceFile": "official_round2.go", + "wireTest": "official_round2_test.go", + "requiredGate": "Exact combined-head native CI and physical package qualification." + }, + "python": { + "sourceFile": "src/polymorfa/official_round2.py", + "wireTest": "tests/test_official_round2.py", + "requiredGate": "Exact combined-head native CI and physical package qualification." + }, + "php": { + "sourceFile": "src/Resources/Catalogs.php", + "wireTest": "tests/official_round2.php", + "requiredGate": "Exact combined-head native CI, physical package and snippet compilation. PHP/Rust execution unavailable locally." + }, + "rust": { + "sourceFile": "src/official_round2.rs", + "wireTest": "tests/official_round2.rs", + "requiredGate": "Exact combined-head native CI, physical package and snippet compilation. PHP/Rust execution unavailable locally." + }, + "dotnet": { + "sourceFile": "src/Polymorfa.Sdk/OfficialRound2.cs", + "wireTest": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs", + "requiredGate": "Exact combined-head native CI and physical package qualification." + } + }, + "platformBatching": { + "snapshot": "contracts/official-api-round2/oa-30.json", + "operations": [ + "listOrganizationWebhooks", + "getOrganizationWebhook", + "createOrganizationWebhook", + "updateOrganizationWebhook", + "listProjectWebhooks", + "getProjectWebhook", + "createProjectWebhook", + "updateProjectWebhook" + ], + "languages": { + "typescript": { + "test": "test/official-api-round2.test.ts" + }, + "go": { + "test": "official_round2_test.go" + }, + "python": { + "test": "tests/test_official_round2.py" + }, + "php": { + "test": "tests/official_round2.php" + }, + "rust": { + "test": "tests/official_round2.rs" + }, + "dotnet": { + "test": "tests/Polymorfa.Sdk.Tests/OfficialRound2Tests.cs" + } + }, + "note": "Existing typed Platform resources extended. Messaging webhook settings unchanged. Preserve omitted PATCH settings and explicit false; API validates saved format." + } +} diff --git a/contracts/official-api-round2.json b/contracts/official-api-round2.json new file mode 100644 index 00000000..7b795a50 --- /dev/null +++ b/contracts/official-api-round2.json @@ -0,0 +1,127 @@ +{ + "repository": "polymorfa/polymorfa", + "sourceCommit": "9bb1e2ed92d8b4a07ff727f8e7ae185498f0868f", + "family": "messaging", + "sourcePath": "apps/api/docs/openapi.json", + "sourceSha256": "6d5b5477a86a85504f921933bf7b26f2c47506e21380fe73c8a5ff1002163b28", + "published": false, + "note": "Unreleased beta source and native wire fixtures in all six languages. Historical per-item merges and snapshots remain authoritative. Exact combined-head hosted qualification, actual SDK357 integration, review, staging/production acceptance and publication remain gates. Baseline API171a/2026-09-22 unchanged; private Console routes excluded.", + "items": [ + { + "item": "OA-03", + "mergeSha": "682b9980a19de784b9ada565a97440a8b7fb05fb", + "sourcePath": "apps/api/docs/openapi.json", + "sourceSha256": "cfa88b9f4aea0cd2d16347d33818f6a3f5870fa0cb376dd6a97103e6b677db2a", + "releasePath": "internal/docs/releases/2026-10-11-cloud-onboarding-readiness.md", + "releaseSha256": "1cb94749b857cce638b81aecbf34f5dfaf1079fca94bdb1632e24069b47463d0", + "snapshotPath": "contracts/official-api-round2/oa-03.json", + "snapshotSha256": "62641435f1d670607d5b91e8363f42fdc45dc7d669c2e3a8c461c2a01abe337c", + "operationIds": ["getQuickLink", "cancelQuickLink"] + }, + { + "item": "OA-16", + "mergeSha": "7391c22e50d6e238771f3428c22611e100f9a13e", + "sourcePath": "apps/api/docs/openapi.json", + "sourceSha256": "04644a0e0fd11b81d457a7f5efb55648567e382953aa2cb93fc7f7250aeff16a", + "releasePath": "internal/docs/releases/2026-10-11-cloud-catalog-sync.md", + "releaseSha256": "dfc1a16d31be23926a1440b5ded807251ba9ca4a7b732c99152ffd0d744aa30f", + "snapshotPath": "contracts/official-api-round2/oa-16.json", + "snapshotSha256": "2bc4fbba821e9c3c90b7a9f632aace0360edbcaf8a1685b9b6a4d9387b5d305d", + "operationIds": [ + "listCloudCatalogs", + "getCloudCatalog", + "syncCloudCatalog", + "listCloudCatalogProducts" + ] + }, + { + "item": "OA-17", + "mergeSha": "24ac44aad59715a3f32dcbee2f4fe5cb07bbda87", + "sourcePath": "apps/api/docs/openapi.json", + "sourceSha256": "5ce4bfda4d9ac8ca29b72a9eff6cf009cc0ee0a8be0167e820d3c851ca3e6f8e", + "releasePath": "internal/docs/releases/2026-10-11-cloud-marketing-messages.md", + "releaseSha256": "088feba9c5dbb933f7a8f159ea7f9f1c6489fbcf70da82277c1f3bd7f04695f4", + "snapshotPath": "contracts/official-api-round2/oa-17.json", + "snapshotSha256": "84f218fa062e92c5792e9f075d1049b26f314634412da2db8b49e195d53d3dee", + "operationIds": ["getMarketingMessagesStatus"] + }, + { + "item": "OA-18", + "mergeSha": "e092ffc559b0bd814931560059e07983c6915f66", + "sourcePath": "apps/api/docs/openapi.json", + "sourceSha256": "7520f63ea6f8b6ca02465b4548c44b5001b0b0b0fadde331d06c4b497bc0153c", + "releasePath": "internal/docs/releases/2026-10-16-oa18-parent-bsuid-block.md", + "releaseSha256": "aea935f32779f2c4d32ca95427746203b5d51cca63f07e4d5454906d6acbe28a", + "snapshotPath": "contracts/official-api-round2/oa-18.json", + "snapshotSha256": "a381304c948807df9ec410617e93ac827073978f091708aac134fe23b7c68f0d", + "operationIds": ["blockContact", "unblockContact"] + }, + { + "item": "OA-21", + "mergeSha": "ceb2d0227e5d0aa21e390508d9437e088794da1f", + "sourcePath": "apps/api/docs/openapi.json", + "sourceSha256": "9d8faf13be970bee800ad15e308c130bc4ae8703865ae99278b9ae315cc27638", + "releasePath": "internal/docs/releases/2026-10-01-official-groups.md", + "releaseSha256": "72efd0f27256dd1c7ee04a26e325b6a9051c4a8453d0b554dbce14241fb662d4", + "snapshotPath": "contracts/official-api-round2/oa-21.json", + "snapshotSha256": "ec42f4a70d6d02803eb31864438668f19307b97486a71dcb0c38ac5cc92b13f6", + "operationIds": ["setOfficialGroupPicture"] + }, + { + "item": "OA-30", + "mergeSha": "d1985700bf1738fe2c4fd55b17a1e6d7fd328453", + "sourcePath": "apps/api/docs/openapi.management.json", + "sourceSha256": "e28256daac1e8efd84998d7399b74c3200623c19ac807a4b0730523a667976ff", + "releasePath": "internal/docs/releases/2026-10-16-webhook-batching.md", + "releaseSha256": "98c411776b87bcbf6fe6a77b7657472ce1ced305a427e939946454a2f8acce37", + "snapshotPath": "contracts/official-api-round2/oa-30.json", + "snapshotSha256": "b6885a304b5d29d14e06c7b6281f1b55d04041f44a288f6754355b8ae67d01f4", + "operationIds": [ + "listOrganizationWebhooks", + "createOrganizationWebhook", + "getOrganizationWebhook", + "updateOrganizationWebhook", + "deleteOrganizationWebhook", + "rotateOrganizationWebhookSecret", + "testOrganizationWebhook", + "listProjectWebhooks", + "createProjectWebhook", + "getProjectWebhook", + "updateProjectWebhook", + "deleteProjectWebhook", + "rotateProjectWebhookSecret", + "testProjectWebhook" + ] + } + ], + "errorCodes": [ + "whatsapp_authorization_required", + "whatsapp_payment_method_required", + "whatsapp_number_not_registered", + "marketing_disabled_on_cloud_api", + "parent_bsuid_not_supported", + "hybrid_group_linked_devices_unavailable", + "catalog_cache_full", + "catalog_sync_cooldown" + ], + "checkedDev": "dafb7cb4e4efbcdcbd04af206f1ef6d47c209379", + "nativeInventory": "contracts/native-official-round2.json", + "managementSource": { + "sourceCommit": "9bb1e2ed92d8b4a07ff727f8e7ae185498f0868f", + "sourcePath": "apps/api/docs/openapi.management.json", + "sourceSha256": "bf4c6056b75e2fb0293b48edc5d8c12c36cdcbd194b9d11159f59522d30badc3" + }, + "graphMarketingSource": { + "sourceCommit": "24ac44aad59715a3f32dcbee2f4fe5cb07bbda87", + "sourcePath": "apps/api/packages/api/src/routes/graph/marketing.ts", + "sourceSha256": "f797f502990c8cabd4ee3a6c4da1ffd644ccbf62f45a204f63b9f0d9029b99cb", + "accepted9bbIdentical": true, + "fields": [ + "marketing_messages_onboarding_status", + "owner_business_info.marketing_messages_onboarding_status.status", + "owner_business_info.marketing_messages_onboarding_status.time", + "disable_marketing_messages_on_cloud_api" + ], + "note": "OA17 filtered public Graph status DTO replaces the retired MM Lite field. Existing Graph catalog/encryption pins remain separate." + } +} diff --git a/contracts/official-api-round2/oa-03.json b/contracts/official-api-round2/oa-03.json new file mode 100644 index 00000000..3d9cbf76 --- /dev/null +++ b/contracts/official-api-round2/oa-03.json @@ -0,0 +1,816 @@ +{ + "paths": { + "/messaging/quicklinks/{id}": { + "get": { + "operationId": "getQuickLink", + "tags": ["QuickLink"], + "summary": "Read QuickLink status", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "quicklink:manage", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Current QuickLink status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuickLinkStatusResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "quicklink:manage" + }, + "description": "**Required scope:** `quicklink:manage`" + }, + "delete": { + "operationId": "cancelQuickLink", + "tags": ["QuickLink"], + "summary": "Cancel a QuickLink", + "description": "**Required scope:** `quicklink:manage`\n\nCancels pending onboarding. Initial Number cleanup is accepted asynchronously and completes only after runtime teardown. Supplementary and reauthorization links cancel only their pending attempts; the existing Number remains. Accepted provider work must finish before cancellation.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "quicklink:manage", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Cancelled", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "message": { + "type": "string" + } + }, + "required": ["success", "message"] + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "202": { + "description": "QuickLink invalidated; Number cleanup pending", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "message": { + "type": "string" + } + }, + "required": ["success", "message"] + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "quicklink:manage" + } + } + } + }, + "components": { + "schemas": { + "QuickLinkStatus": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "purpose": { + "type": "string", + "enum": ["initial", "add_connection", "reauthorization"] + }, + "connectionGoal": { + "type": "string", + "enum": ["single", "hybrid"] + }, + "addConnection": { + "type": "string", + "enum": ["linked_devices", "official_api"] + }, + "status": { + "type": "string", + "enum": [ + "pending", + "opened", + "linked", + "connected", + "failed", + "cancelled" + ], + "description": "`linked`: for Linked Devices, the device is linked and the recipient has not confirmed; for Meta Cloud API, the number is connected to Meta and is still being made ready to send. For Meta Cloud API, `connected` requires every readiness check to pass, a successful runtime connection, and the recipient's confirmation. `failed` carries the reason in `errorCode`." + }, + "session": { + "type": "string" + }, + "expiresAt": { + "type": "string", + "nullable": true + }, + "openedAt": { + "type": "string", + "nullable": true + }, + "connectedAt": { + "type": "string", + "nullable": true + }, + "phone": { + "type": "string", + "nullable": true, + "description": "Connected phone in E.164 once the link is connected." + }, + "errorCode": { + "type": "string", + "nullable": true + }, + "hybridPhase": { + "type": "string", + "nullable": true, + "enum": [ + "cloud_setup", + "linked_pairing", + "repair_linked", + "ready", + null + ] + }, + "onboarding": { + "type": "object", + "nullable": true, + "properties": { + "stage": { + "type": "string" + }, + "connection": { + "type": "string", + "nullable": true + }, + "coexistence": { + "type": "boolean", + "nullable": true + }, + "contactsSync": { + "type": "string" + }, + "historySync": { + "type": "string" + }, + "historyProgress": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "errorCode": { + "type": "string", + "nullable": true + }, + "sync": { + "$ref": "#/components/schemas/CloudSyncStatus" + } + }, + "required": [ + "stage", + "connection", + "coexistence", + "contactsSync", + "historySync", + "historyProgress", + "errorCode", + "sync" + ], + "description": "Durable connection and sync progress; omitted when the onboarding service is unavailable, null when no record exists. Sync request and delivery evidence are independent of the connection stage." + } + }, + "required": [ + "id", + "purpose", + "connectionGoal", + "status", + "session", + "expiresAt", + "openedAt", + "connectedAt", + "phone", + "errorCode", + "hybridPhase" + ] + }, + "CloudSyncStatus": { + "type": "object", + "properties": { + "contacts": { + "type": "object", + "properties": { + "request": { + "type": "string", + "enum": [ + "not_applicable", + "not_requested", + "pending", + "requesting", + "accepted", + "declined", + "unknown" + ], + "description": "Accepted means Meta returned a request receipt; it does not prove all contacts were delivered. Unknown means a one-time request may have been accepted without a saved receipt." + }, + "receiptRecorded": { + "type": "boolean" + } + }, + "required": ["request", "receiptRecorded"] + }, + "history": { + "type": "object", + "properties": { + "request": { + "type": "string", + "enum": [ + "not_applicable", + "not_requested", + "pending", + "requesting", + "accepted", + "declined", + "unknown" + ] + }, + "receiptRecorded": { + "type": "boolean" + }, + "delivery": { + "type": "string", + "enum": [ + "not_applicable", + "not_requested", + "unconfirmed", + "partial", + "complete", + "declined" + ], + "description": "Partial and complete describe the latest history progress reported by Meta. Unconfirmed does not mean that no history exists." + } + }, + "required": ["request", "receiptRecorded", "delivery"] + } + }, + "required": ["contacts", "history"] + }, + "PublicError": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "invalid_request_error", + "authentication_error", + "permission_error", + "conflict_error", + "rate_limit_error", + "api_error", + "upstream_error" + ] + }, + "code": { + "type": "string", + "enum": [ + "invalid_parameter", + "invalid_credential", + "entitlement_required", + "cloud_template_authority_restricted", + "gate_limit_reached", + "method_not_allowed", + "permission_denied", + "missing_scope", + "resource_not_found", + "hms_not_enabled", + "media_fetch_failed", + "resource_gone", + "feature_unavailable", + "stream_cursor_invalid", + "stream_cursor_expired", + "stream_connection_limit_reached", + "state_conflict", + "team_deletion_scheduled", + "hybrid_routing_paused", + "hybrid_choice_required", + "hybrid_transition_ineligible", + "hybrid_automation_unavailable", + "hybrid_transport_unavailable", + "hybrid_target_reference_unavailable", + "send_outcome_unknown", + "idempotency_conflict", + "idempotency_in_progress", + "idempotency_completed", + "idempotency_outcome_unknown", + "call_claimed", + "call_not_ringing", + "connection_limit", + "unsupported_for_connection", + "call_permission_required", + "invalid_sip_trunk", + "calls_disabled", + "call_recipient_opted_out", + "call_destination_blocked", + "call_permission_request_limited", + "call_permission_granted", + "order_status_transition_invalid", + "order_cancellation_failed", + "call_opt_out_limit", + "number_restricted", + "sip_trunk_in_use", + "sip_trunk_revision_conflict", + "sip_trunk_limit", + "sip_unavailable", + "voice_not_enabled", + "provider_credential_invalid", + "provider_unavailable", + "asset_not_ready", + "voice_asset_in_use", + "voice_asset_revision_conflict", + "voice_unavailable", + "rate_limit_exceeded", + "internal_error", + "operation_not_supported", + "upstream_failure", + "service_unavailable", + "credential_verification_unavailable", + "session_not_ready", + "command_dispatch_failed", + "result_unknown", + "parent_bsuid_not_supported", + "recipient_not_on_whatsapp", + "conversation_window_closed", + "template_not_approved", + "media_too_large", + "whatsapp_rate_limited", + "new_chat_limit_reached", + "whatsapp_account_restricted", + "whatsapp_authorization_required", + "whatsapp_payment_method_required", + "whatsapp_number_not_registered", + "whatsapp_groups_ineligible", + "whatsapp_group_creation_paused", + "whatsapp_group_limit_reached", + "whatsapp_group_full", + "whatsapp_group_suspended", + "whatsapp_group_has_no_participants", + "bansafe_suspended", + "bansafe_org_suspended", + "bansafe_cold_blocked", + "bansafe_cold_held", + "bansafe_throttled", + "bansafe_daily_allowance_reached", + "bansafe_accounting_unavailable", + "bansafe_send_outcome_unknown", + "campaigns_not_entitled", + "campaign_throughput_capped", + "campaign_approval_required", + "campaign_approval_stale", + "campaign_variables_missing" + ] + }, + "message": { + "type": "string" + }, + "param": { + "type": "string", + "nullable": true + }, + "request_id": { + "type": "string", + "description": "Matches the X-Request-Id response header. Include it when you contact support." + }, + "request_log_url": { + "type": "string", + "format": "uri", + "description": "Console page that shows this request. Returned only to organization and project credentials whose request was logged." + } + }, + "required": ["type", "code", "message", "param", "request_id"] + }, + "data": { + "type": "string", + "nullable": true, + "enum": [null], + "description": "Always null for a failed request." + }, + "docs": { + "type": "string", + "format": "uri" + } + }, + "required": ["error", "data", "docs"] + }, + "QuickLinkStatusResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "$ref": "#/components/schemas/QuickLinkStatus" + } + }, + "required": ["success", "data"] + } + } + } +} diff --git a/contracts/official-api-round2/oa-16.json b/contracts/official-api-round2/oa-16.json new file mode 100644 index 00000000..d907e30c --- /dev/null +++ b/contracts/official-api-round2/oa-16.json @@ -0,0 +1,2003 @@ +{ + "paths": { + "/messaging/{session}/catalogs": { + "get": { + "operationId": "listCloudCatalogs", + "tags": ["Catalogs"], + "summary": "List synced catalogs for an Official API Number", + "description": "**Protocols:** Meta Cloud API\n\n**Required scope:** `business:read`\n\nReturns the catalogs this project has synced for the WhatsApp Business Account of the Number's active Official connection, with each sync's state. It does not call Meta.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "business:read", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Synced catalogs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListCloudCatalogsResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "503": { + "description": "Catalog access or the Number's WhatsApp Business Account is unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "service_unavailable", + "message": "A required service is unavailable. Check completion before repeating a write.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#service-unavailable" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "Meta Cloud API business:read" + }, + "x-protocols": ["Meta Cloud API"] + } + }, + "/messaging/{session}/catalogs/{catalogId}": { + "get": { + "operationId": "getCloudCatalog", + "tags": ["Catalogs"], + "summary": "Get a catalog's sync state", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "business:read", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[0-9]{1,32}$", + "description": "Meta catalog ID" + }, + "required": true, + "name": "catalogId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Sync state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCloudCatalogResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "503": { + "description": "Catalog access or the Number's WhatsApp Business Account is unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "service_unavailable", + "message": "A required service is unavailable. Check completion before repeating a write.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#service-unavailable" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "Meta Cloud API business:read" + }, + "x-protocols": ["Meta Cloud API"], + "description": "**Protocols:** Meta Cloud API\n\n**Required scope:** `business:read`" + } + }, + "/messaging/{session}/catalogs/{catalogId}/sync": { + "post": { + "operationId": "syncCloudCatalog", + "tags": ["Catalogs"], + "summary": "Sync a linked catalog from Meta", + "description": "**Protocols:** Meta Cloud API\n\n**Required scope:** `business:read`\n\nReads every product of a catalog linked to the Number's WhatsApp Business Account, up to 5,000, and replaces the stored copy only when the read is complete. A failed or incomplete read leaves the stored products unchanged. One sync per catalog can start every 2 minutes.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "business:read", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[0-9]{1,32}$", + "description": "Meta catalog ID" + }, + "required": true, + "name": "catalogId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The stored products were replaced", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncCloudCatalogResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "The Number was not found, or Meta does not list the catalog for this WhatsApp Business Account (`catalog_not_linked`). Stored products for it are removed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "409": { + "description": "A newer sync replaced this one (`catalog_sync_superseded`)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "conflict_error", + "code": "state_conflict", + "message": "The request conflicts with the resource state.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#state-conflict" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "422": { + "description": "The catalog exceeds 5,000 products (`catalog_too_large`), or the project already caches 100 catalogs (`catalog_cache_full`). Stored products are unchanged.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "429": { + "description": "A sync for this catalog started less than 2 minutes ago (`catalog_sync_cooldown`). Retry after `retryAfter` seconds.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "rate_limit_error", + "code": "rate_limit_exceeded", + "message": "The request limit was reached. Follow Retry-After when supplied.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#rate-limit-exceeded" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "502": { + "description": "Meta refused or did not complete the read (`catalog_sync_incomplete`). Stored products are unchanged.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "upstream_error", + "code": "upstream_failure", + "message": "The messaging provider could not complete the request.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#upstream-failure" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "503": { + "description": "Catalog access or the Number's WhatsApp Business Account is unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "service_unavailable", + "message": "A required service is unavailable. Check completion before repeating a write.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#service-unavailable" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "Meta Cloud API business:read" + }, + "x-protocols": ["Meta Cloud API"] + } + }, + "/messaging/{session}/catalogs/{catalogId}/products": { + "get": { + "operationId": "listCloudCatalogProducts", + "tags": ["Catalogs"], + "summary": "List stored products of a synced catalog", + "description": "**Protocols:** Meta Cloud API\n\n**Required scope:** `business:read`\n\nPages through the products stored by the last complete sync, in product ID order. Check `catalog.stale` and sync again when you need current data. It does not call Meta.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "business:read", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[0-9]{1,32}$", + "description": "Meta catalog ID" + }, + "required": true, + "name": "catalogId", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Page size, 1-100 (default 50)" + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,256}$", + "description": "`nextCursor` from the previous page" + }, + "required": false, + "name": "cursor", + "in": "query" + } + ], + "responses": { + "200": { + "description": "One page of stored products", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListCloudCatalogProductsResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "409": { + "description": "The catalog changed after the previous page (`catalog_snapshot_changed`). Restart without a cursor.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "conflict_error", + "code": "state_conflict", + "message": "The request conflicts with the resource state.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#state-conflict" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "503": { + "description": "Catalog access or the Number's WhatsApp Business Account is unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "service_unavailable", + "message": "A required service is unavailable. Check completion before repeating a write.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#service-unavailable" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "Meta Cloud API business:read" + }, + "x-protocols": ["Meta Cloud API"] + } + } + }, + "components": { + "schemas": { + "CloudMessagePayload": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "whatsapp_ids": { + "$ref": "#/components/schemas/WhatsAppMessageIds" + }, + "whatsapp_id": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "Temporary singular provider reference for older consumers. Use whatsapp_ids for transport-specific references.", + "deprecated": true + }, + "conversation": { + "$ref": "#/components/schemas/ConversationRef" + }, + "timestamp": { + "type": "string" + }, + "type": { + "type": "string" + }, + "senderName": { + "type": "string" + }, + "nativeFlowResponse": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "paramsJson": { + "type": "string", + "minLength": 1, + "maxLength": 262144 + }, + "version": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["name", "paramsJson"] + }, + "replyChoice": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["button", "list"], + "description": "Reply control the contact used: a reply or quick-reply button, or a list row." + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "ID you assigned to the selected button or list row. It is never the visible label, except for a template quick-reply button sent without a payload, where WhatsApp returns the button text." + } + }, + "required": ["kind", "id"], + "description": "Present when the contact selects a reply button or list row." + }, + "parentMessageId": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "WhatsApp ID of the message this one quotes, when WhatsApp supplies one. It is not necessarily a message you sent." + }, + "interactive": { + "type": "object", + "additionalProperties": {} + }, + "order": { + "$ref": "#/components/schemas/CloudOrder" + }, + "referral": { + "type": "object", + "properties": { + "source_type": { + "type": "string" + }, + "source_id": { + "type": "string" + }, + "source_url": { + "type": "string" + }, + "ctwa_clid": { + "type": "string" + } + }, + "description": "Meta-supplied Click-to-WhatsApp entry point. Does not establish a conversion or payment." + } + }, + "required": ["id", "whatsapp_ids", "conversation", "timestamp", "type"] + }, + "HistoryMessage": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "whatsapp_ids": { + "$ref": "#/components/schemas/WhatsAppMessageIds" + }, + "whatsapp_id": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "Temporary singular provider reference for older consumers. Use whatsapp_ids for transport-specific references.", + "deprecated": true + }, + "conversation": { + "$ref": "#/components/schemas/HistoryConversation" + }, + "direction": { + "type": "string", + "enum": ["inbound", "outbound"] + }, + "fromMe": { + "type": "boolean", + "description": "True when the Number sent the message, including from another linked device." + }, + "type": { + "type": "string", + "description": "Message type as delivered in `message.received`, for example `text` or `image`." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When WhatsApp reported the message." + }, + "pushName": { + "type": "string" + }, + "text": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "ptt": { + "type": "boolean" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "displayName": { + "type": "string" + }, + "title": { + "type": "string" + }, + "reaction": { + "type": "string" + }, + "reactionTo": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "edited": { + "type": "boolean" + }, + "unavailable": { + "type": "boolean" + }, + "unavailableReason": { + "type": "string" + }, + "pollOptions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "hash": { + "type": "string" + } + }, + "required": ["name", "hash"] + } + }, + "order": { + "allOf": [ + { + "$ref": "#/components/schemas/CloudOrder" + }, + { + "description": "Official API `order` messages: the cart as Meta reported it, when every line is readable." + } + ] + }, + "media": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HistoryMedia" + }, + "description": "Downloadable media for image, video, audio, document, and sticker messages. Empty when no copy is available." + }, + "mediaRetrieval": { + "$ref": "#/components/schemas/HistoryMediaRetrieval" + } + }, + "required": [ + "id", + "whatsapp_ids", + "conversation", + "direction", + "fromMe", + "type", + "timestamp" + ] + }, + "HistoryMediaRetrieval": { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "pending", + "stored", + "unavailable", + "expired", + "too_large", + "unsupported_type", + "failed", + "cancelled" + ], + "description": "`pending`: the copy is not stored yet. `stored`: `media` lists the copy. Every other state is final and no copy will be stored." + }, + "reason": { + "type": "string", + "description": "Why a final state other than `stored` was reached, for example `provider_media_not_found` or `media_too_large`.", + "example": "provider_media_expired" + } + }, + "required": ["state"], + "description": "Official API media only: whether a copy of the media was stored with this message." + }, + "HistoryMedia": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Media ID. Download the bytes from `url` with a credential that has `media:read`." + }, + "mimeType": { + "type": "string" + }, + "fileLength": { + "type": "integer", + "minimum": 0 + }, + "url": { + "type": "string", + "description": "Path of the media download method.", + "example": "/messaging/media/0e6f1c3a-5a55-4f3c-9d4f-0b1f8f2a9c11" + } + }, + "required": ["id", "mimeType", "fileLength", "url"] + }, + "CloudOrder": { + "type": "object", + "properties": { + "catalogId": { + "type": "string", + "pattern": "^[0-9]{1,32}$", + "description": "Meta catalog ID the cart was built from." + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "Text the contact sent with the order." + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CloudOrderItem" + }, + "minItems": 1, + "maxItems": 1000 + } + }, + "required": ["catalogId", "items"], + "description": "Present on `type: \"order\"` messages when every cart line is readable. There is no order total: multiply `quantity` by `itemPrice` per item if you need one. Prices and availability can differ from your catalog; confirm them before you fulfil the order." + }, + "CloudOrderItem": { + "type": "object", + "properties": { + "productRetailerId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Your retailer ID (SKU), at most 256 UTF-8 bytes with no control characters." + }, + "quantity": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "description": "Units ordered." + }, + "itemPrice": { + "type": "number", + "minimum": 0, + "description": "Unit price as Meta reported it. Polymorfa does not convert or recompute it." + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$", + "description": "ISO 4217 currency code Meta reported for the item." + } + }, + "required": ["productRetailerId", "quantity", "itemPrice", "currency"] + }, + "HistoryConversation": { + "allOf": [ + { + "$ref": "#/components/schemas/ConversationIdentity" + }, + { + "type": "object", + "properties": { + "sender": { + "allOf": [ + { + "$ref": "#/components/schemas/ConversationIdentity" + }, + { + "description": "Author of an inbound group message." + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "ConversationIdentity": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "phoneNumber": { + "type": "string", + "pattern": "^\\+[1-9]\\d{1,14}$" + }, + "bsuid": { + "type": "string", + "pattern": "^[A-Z]{2}\\.[A-Za-z0-9]{1,128}$" + }, + "username": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": ["id"], + "additionalProperties": false + }, + "WhatsAppMessageIds": { + "anyOf": [ + { + "type": "object", + "properties": { + "linked_devices": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "official_api": { + "type": "string", + "minLength": 1, + "maxLength": 512 + } + }, + "required": ["linked_devices"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "linked_devices": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "official_api": { + "type": "string", + "minLength": 1, + "maxLength": 512 + } + }, + "required": ["official_api"], + "additionalProperties": false + } + ], + "description": "Exact message references observed through Linked Devices, the Official API, or both. Use the Polymorfa message id for actions and replies. Unknown provider references are omitted." + }, + "ConversationRef": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "phoneNumber": { + "type": "string" + }, + "bsuid": { + "type": "string" + }, + "username": { + "type": "string" + }, + "parentBsuid": { + "type": "string", + "description": "Parent business-scoped user ID (`CC.ENT.…`), present only when WhatsApp reports one for this person. It identifies the person across the business portfolios enrolled in one parent account. It cannot be used to send messages or block people; use `id` or `bsuid`.", + "example": "US.ENT.11815799212886844830" + }, + "sender": { + "$ref": "#/components/schemas/IdentityRef" + } + }, + "required": ["id"] + }, + "IdentityRef": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "phoneNumber": { + "type": "string" + }, + "bsuid": { + "type": "string" + }, + "username": { + "type": "string" + }, + "parentBsuid": { + "type": "string", + "description": "Parent business-scoped user ID (`CC.ENT.…`), present only when WhatsApp reports one for this person. It identifies the person across the business portfolios enrolled in one parent account. It cannot be used to send messages or block people; use `id` or `bsuid`.", + "example": "US.ENT.11815799212886844830" + } + }, + "required": ["id"] + }, + "PublicError": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "invalid_request_error", + "authentication_error", + "permission_error", + "conflict_error", + "rate_limit_error", + "api_error", + "upstream_error" + ] + }, + "code": { + "type": "string", + "enum": [ + "invalid_parameter", + "invalid_credential", + "entitlement_required", + "cloud_template_authority_restricted", + "gate_limit_reached", + "method_not_allowed", + "permission_denied", + "missing_scope", + "resource_not_found", + "hms_not_enabled", + "media_fetch_failed", + "resource_gone", + "feature_unavailable", + "stream_cursor_invalid", + "stream_cursor_expired", + "stream_connection_limit_reached", + "state_conflict", + "team_deletion_scheduled", + "hybrid_routing_paused", + "hybrid_choice_required", + "hybrid_transition_ineligible", + "hybrid_automation_unavailable", + "hybrid_transport_unavailable", + "hybrid_target_reference_unavailable", + "send_outcome_unknown", + "idempotency_conflict", + "idempotency_in_progress", + "idempotency_completed", + "idempotency_outcome_unknown", + "call_claimed", + "call_not_ringing", + "connection_limit", + "unsupported_for_connection", + "call_permission_required", + "invalid_sip_trunk", + "calls_disabled", + "call_recipient_opted_out", + "call_destination_blocked", + "call_permission_request_limited", + "call_permission_granted", + "order_status_transition_invalid", + "order_cancellation_failed", + "call_opt_out_limit", + "number_restricted", + "sip_trunk_in_use", + "sip_trunk_revision_conflict", + "sip_trunk_limit", + "sip_unavailable", + "voice_not_enabled", + "provider_credential_invalid", + "provider_unavailable", + "asset_not_ready", + "voice_asset_in_use", + "voice_asset_revision_conflict", + "voice_unavailable", + "rate_limit_exceeded", + "internal_error", + "operation_not_supported", + "upstream_failure", + "service_unavailable", + "credential_verification_unavailable", + "session_not_ready", + "command_dispatch_failed", + "result_unknown", + "parent_bsuid_not_supported", + "recipient_not_on_whatsapp", + "conversation_window_closed", + "template_not_approved", + "media_too_large", + "whatsapp_rate_limited", + "new_chat_limit_reached", + "whatsapp_account_restricted", + "whatsapp_authorization_required", + "whatsapp_payment_method_required", + "whatsapp_number_not_registered", + "whatsapp_groups_ineligible", + "whatsapp_group_creation_paused", + "whatsapp_group_limit_reached", + "whatsapp_group_full", + "whatsapp_group_suspended", + "whatsapp_group_has_no_participants", + "bansafe_suspended", + "bansafe_org_suspended", + "bansafe_cold_blocked", + "bansafe_cold_held", + "bansafe_throttled", + "bansafe_daily_allowance_reached", + "bansafe_accounting_unavailable", + "bansafe_send_outcome_unknown", + "campaigns_not_entitled", + "campaign_throughput_capped", + "campaign_approval_required", + "campaign_approval_stale", + "campaign_variables_missing" + ] + }, + "message": { + "type": "string" + }, + "param": { + "type": "string", + "nullable": true + }, + "request_id": { + "type": "string", + "description": "Matches the X-Request-Id response header. Include it when you contact support." + }, + "request_log_url": { + "type": "string", + "format": "uri", + "description": "Console page that shows this request. Returned only to organization and project credentials whose request was logged." + } + }, + "required": ["type", "code", "message", "param", "request_id"] + }, + "data": { + "type": "string", + "nullable": true, + "enum": [null], + "description": "Always null for a failed request." + }, + "docs": { + "type": "string", + "format": "uri" + } + }, + "required": ["error", "data", "docs"] + }, + "ListCloudCatalogProductsResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "$ref": "#/components/schemas/CloudCatalogProductPage" + } + }, + "required": ["success", "data"] + }, + "CloudCatalogProductPage": { + "type": "object", + "properties": { + "catalog": { + "$ref": "#/components/schemas/CloudCatalogSync" + }, + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CloudCatalogProduct" + } + }, + "nextCursor": { + "type": "string", + "description": "Pass as `cursor` for the next page. Absent on the last page." + } + }, + "required": ["catalog", "products"] + }, + "CloudCatalogProduct": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Meta product item ID" + }, + "retailerId": { + "type": "string", + "description": "Your retailer ID (SKU). Use it in product and product list messages." + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "price": { + "type": "string", + "description": "Price exactly as Meta returned it." + }, + "currency": { + "type": "string" + }, + "availability": { + "type": "string" + }, + "reviewStatus": { + "type": "string" + }, + "visibility": { + "type": "string" + }, + "imageUrl": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["id"] + }, + "CloudCatalogSync": { + "type": "object", + "properties": { + "catalogId": { + "type": "string" + }, + "syncedAt": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "When the stored products were last read completely from Meta. Null before the first complete read." + }, + "stale": { + "type": "boolean", + "description": "True when there is no complete read or the last one is more than 24 hours old. Polymorfa does not refresh the copy on its own." + }, + "productCount": { + "type": "integer", + "minimum": 0 + }, + "lastAttempt": { + "type": "object", + "properties": { + "startedAt": { + "type": "string", + "format": "date-time" + }, + "finishedAt": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "outcome": { + "type": "string", + "enum": [ + "in_progress", + "complete", + "provider_error", + "unavailable", + "too_large", + "not_linked" + ], + "description": "`complete` replaced the stored products. `provider_error`, `unavailable` and `too_large` left them unchanged. `not_linked` removed them because Meta no longer lists the catalog for this account." + }, + "metaErrorCode": { + "type": "integer", + "description": "Meta's error code for `provider_error`." + } + }, + "required": ["startedAt", "finishedAt", "outcome"] + } + }, + "required": [ + "catalogId", + "syncedAt", + "stale", + "productCount", + "lastAttempt" + ] + }, + "SyncCloudCatalogResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "$ref": "#/components/schemas/CloudCatalogSync" + } + }, + "required": ["success", "data"] + }, + "GetCloudCatalogResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "$ref": "#/components/schemas/CloudCatalogSync" + } + }, + "required": ["success", "data"] + }, + "ListCloudCatalogsResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "type": "object", + "properties": { + "catalogs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CloudCatalogSync" + } + } + }, + "required": ["catalogs"] + } + }, + "required": ["success", "data"] + } + } + } +} diff --git a/contracts/official-api-round2/oa-17.json b/contracts/official-api-round2/oa-17.json new file mode 100644 index 00000000..921b2927 --- /dev/null +++ b/contracts/official-api-round2/oa-17.json @@ -0,0 +1,558 @@ +{ + "paths": { + "/messaging/{session}/marketing-messages/status": { + "get": { + "operationId": "getMarketingMessagesStatus", + "tags": ["Marketing Messages"], + "summary": "Get Marketing Messages onboarding status", + "description": "**Protocols:** Meta Cloud API\n\n**Required scope:** `sessions:read`\n\nReads WhatsApp's Marketing Messages onboarding status, terms of service state and Cloud API marketing setting for the Number's WhatsApp Business Account. The status can change between this read and a send; WhatsApp decides each send. Beta: requires team enrollment and a Number with an Official API connection.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "sessions:read", + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Number ID." + }, + "required": true, + "name": "session", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMarketingMessagesStatusResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "The Number ID fails validation (`invalid_parameter`); it must contain 1 to 128 characters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "The credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "The credential lacks `sessions:read`, or the team is not enrolled in the beta (`feature_unavailable`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "The Number is unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "409": { + "description": "The Number has no connected Official API account, for example a Linked Devices or test Number (`unsupported_for_connection`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "502": { + "description": "WhatsApp refused the read or its answer could not be read (`upstream_failure`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "503": { + "description": "WhatsApp or a required service is unavailable (`service_unavailable`). Repeating the read is safe.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "Meta Cloud API sessions:read" + }, + "x-protocols": ["Meta Cloud API"] + } + } + }, + "components": { + "schemas": { + "MarketingOnboardingUpdatedEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique event identifier" + }, + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "externalId": { + "type": "string", + "description": "Integrator reference from the QuickLink that created this session. Included on session lifecycle events when supplied." + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "enum": ["marketing.onboarding_updated"], + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/MarketingOnboardingUpdatedPayload" + } + }, + "required": ["id", "session", "timestamp", "event", "payload"] + }, + "ContactMarketingPreferenceUpdatedEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique event identifier" + }, + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "externalId": { + "type": "string", + "description": "Integrator reference from the QuickLink that created this session. Included on session lifecycle events when supplied." + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "enum": ["contact.marketing_preference_updated"], + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/ContactMarketingPreferencePayload" + } + }, + "required": ["id", "session", "timestamp", "event", "payload"] + }, + "ContactMarketingPreferencePayload": { + "type": "object", + "properties": { + "waId": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "WhatsApp user ID, preserved unchanged. It is not necessarily a phone number" + }, + "preference": { + "type": "string", + "enum": ["stop", "resume"], + "description": "`stop`: the person stopped marketing messages from your business; later marketing messages to them fail with WhatsApp error 131050 until they resume. Do not resend to them. `resume`: they accept marketing messages again" + }, + "category": { + "type": "string", + "enum": ["marketing_messages"] + }, + "phoneNumberId": { + "type": "string", + "description": "WhatsApp phone number ID of your Number" + } + }, + "required": ["waId", "preference", "category"] + }, + "MarketingOnboardingUpdatedPayload": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["terms_signed", "ad_account_linked"], + "description": "`terms_signed`: the business accepted the Marketing Messages terms for this WhatsApp Business Account. `ad_account_linked`: WhatsApp's older onboarding completion notice, which WhatsApp is retiring" + }, + "wabaId": { + "type": "string", + "description": "WhatsApp Business Account the notice is about" + }, + "ownerBusinessId": { + "type": "string", + "description": "Business portfolio that owns the account" + }, + "metaEvent": { + "type": "string", + "enum": ["MM_LITE_TERMS_SIGNED", "AD_ACCOUNT_LINKED"], + "description": "WhatsApp's `account_update` event value" + } + }, + "required": ["kind", "wabaId", "metaEvent"] + }, + "PublicError": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "invalid_request_error", + "authentication_error", + "permission_error", + "conflict_error", + "rate_limit_error", + "api_error", + "upstream_error" + ] + }, + "code": { + "type": "string", + "enum": [ + "invalid_parameter", + "invalid_credential", + "entitlement_required", + "cloud_template_authority_restricted", + "gate_limit_reached", + "method_not_allowed", + "permission_denied", + "missing_scope", + "resource_not_found", + "hms_not_enabled", + "media_fetch_failed", + "resource_gone", + "feature_unavailable", + "stream_cursor_invalid", + "stream_cursor_expired", + "stream_connection_limit_reached", + "state_conflict", + "team_deletion_scheduled", + "hybrid_routing_paused", + "hybrid_choice_required", + "hybrid_transition_ineligible", + "hybrid_automation_unavailable", + "hybrid_transport_unavailable", + "hybrid_target_reference_unavailable", + "send_outcome_unknown", + "idempotency_conflict", + "idempotency_in_progress", + "idempotency_completed", + "idempotency_outcome_unknown", + "call_claimed", + "call_not_ringing", + "connection_limit", + "unsupported_for_connection", + "call_permission_required", + "invalid_sip_trunk", + "calls_disabled", + "call_recipient_opted_out", + "call_destination_blocked", + "call_permission_request_limited", + "call_permission_granted", + "order_status_transition_invalid", + "order_cancellation_failed", + "call_opt_out_limit", + "number_restricted", + "sip_trunk_in_use", + "sip_trunk_revision_conflict", + "sip_trunk_limit", + "sip_unavailable", + "voice_not_enabled", + "provider_credential_invalid", + "provider_unavailable", + "asset_not_ready", + "voice_asset_in_use", + "voice_asset_revision_conflict", + "voice_unavailable", + "rate_limit_exceeded", + "internal_error", + "operation_not_supported", + "upstream_failure", + "service_unavailable", + "credential_verification_unavailable", + "session_not_ready", + "command_dispatch_failed", + "result_unknown", + "parent_bsuid_not_supported", + "recipient_not_on_whatsapp", + "conversation_window_closed", + "template_not_approved", + "media_too_large", + "whatsapp_rate_limited", + "new_chat_limit_reached", + "marketing_disabled_on_cloud_api", + "whatsapp_account_restricted", + "whatsapp_authorization_required", + "whatsapp_payment_method_required", + "whatsapp_number_not_registered", + "whatsapp_groups_ineligible", + "whatsapp_group_creation_paused", + "whatsapp_group_limit_reached", + "whatsapp_group_full", + "whatsapp_group_suspended", + "whatsapp_group_has_no_participants", + "bansafe_suspended", + "bansafe_org_suspended", + "bansafe_cold_blocked", + "bansafe_cold_held", + "bansafe_throttled", + "bansafe_daily_allowance_reached", + "bansafe_accounting_unavailable", + "bansafe_send_outcome_unknown", + "campaigns_not_entitled", + "campaign_throughput_capped", + "campaign_approval_required", + "campaign_approval_stale", + "campaign_variables_missing" + ] + }, + "message": { + "type": "string" + }, + "param": { + "type": "string", + "nullable": true + }, + "request_id": { + "type": "string", + "description": "Matches the X-Request-Id response header. Include it when you contact support." + }, + "request_log_url": { + "type": "string", + "format": "uri", + "description": "Console page that shows this request. Returned only to organization and project credentials whose request was logged." + } + }, + "required": ["type", "code", "message", "param", "request_id"] + }, + "data": { + "type": "string", + "nullable": true, + "enum": [null], + "description": "Always null for a failed request." + }, + "docs": { + "type": "string", + "format": "uri" + } + }, + "required": ["error", "data", "docs"] + }, + "GetMarketingMessagesStatusResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "$ref": "#/components/schemas/MarketingMessagesStatus" + } + }, + "required": ["success", "data"] + }, + "MarketingMessagesStatus": { + "type": "object", + "properties": { + "wabaId": { + "type": "string", + "description": "WhatsApp Business Account of the Number's Official API connection." + }, + "onboarding": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["onboarded", "eligible", "unknown"], + "description": "`onboarded`: WhatsApp reports the account onboarded to Marketing Messages. `eligible`: the account can onboard but has not. `unknown`: WhatsApp returned no status or a value it does not document." + }, + "metaStatus": { + "type": "string", + "nullable": true, + "description": "WhatsApp's `marketing_messages_onboarding_status` value, unchanged.", + "example": "ONBOARDED" + } + }, + "required": ["status", "metaStatus"] + }, + "terms": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["signed", "requested", "not_started", "unknown"], + "description": "Terms of service state of the business portfolio that owns the account." + }, + "metaStatus": { + "type": "string", + "nullable": true, + "description": "WhatsApp's value, unchanged: `TERM_OF_SERVICE_SIGNED`, `REQUEST_SENT` or `NOT_STARTED`.", + "example": "TERM_OF_SERVICE_SIGNED" + }, + "date": { + "type": "string", + "nullable": true, + "description": "Date WhatsApp reports for the terms state (YYYY-MM-DD).", + "example": "2025-08-13" + } + }, + "required": ["status", "metaStatus", "date"] + }, + "cloudApiMarketingDisabled": { + "type": "boolean", + "nullable": true, + "description": "True when the account refuses Marketing templates sent as ordinary messages (`marketing_disabled_on_cloud_api`). Null when WhatsApp did not report it." + }, + "nextAction": { + "type": "string", + "enum": ["none", "accept_terms", "review_in_whatsapp_manager"], + "description": "`accept_terms`: someone with full control of the business portfolio accepts the Marketing Messages terms in WhatsApp Manager (Overview, Alerts). `review_in_whatsapp_manager`: check the account in WhatsApp Manager. There is no API for either step." + }, + "guideUrl": { + "type": "string", + "format": "uri", + "description": "WhatsApp's onboarding guide for the terms step." + }, + "observedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "wabaId", + "onboarding", + "terms", + "cloudApiMarketingDisabled", + "nextAction", + "guideUrl", + "observedAt" + ] + } + } + } +} diff --git a/contracts/official-api-round2/oa-18.json b/contracts/official-api-round2/oa-18.json new file mode 100644 index 00000000..8be9402d --- /dev/null +++ b/contracts/official-api-round2/oa-18.json @@ -0,0 +1,937 @@ +{ + "paths": { + "/messaging/{session}/contacts/{contactId}/block": { + "post": { + "operationId": "blockContact", + "tags": ["Contacts"], + "summary": "Block a contact", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "contacts:manage", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$" + }, + "required": true, + "name": "contactId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Contact blocked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request \u2014 request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "Linked Device API contacts:manage" + }, + "x-protocols": ["Linked Device API"], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `contacts:manage`" + } + }, + "/messaging/{session}/contacts/{contactId}/unblock": { + "post": { + "operationId": "unblockContact", + "tags": ["Contacts"], + "summary": "Unblock a contact", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "contacts:manage", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$" + }, + "required": true, + "name": "contactId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Contact unblocked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "Bad request \u2014 request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "invalid_parameter", + "message": "Correct the invalid request field.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-parameter" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "authentication_error", + "code": "invalid_credential", + "message": "Supply a valid credential.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#invalid-credential" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "permission_error", + "code": "permission_denied", + "message": "The credential does not have access to this resource.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#permission-denied" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "invalid_request_error", + "code": "resource_not_found", + "message": "The resource is unavailable to this caller.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#resource-not-found" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + }, + "example": { + "error": { + "type": "api_error", + "code": "internal_error", + "message": "An unexpected service failure occurred. Keep the request ID for support.", + "param": null, + "request_id": "5f0c2a8e-3b1d-4c6f-9e2a-7d4b1c8f6a30" + }, + "data": null, + "docs": "https://docs.polymorfa.com/api/errors#internal-error" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "Linked Device API contacts:manage" + }, + "x-protocols": ["Linked Device API"], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `contacts:manage`" + } + } + }, + "components": { + "schemas": { + "IdentityRef": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "phoneNumber": { + "type": "string" + }, + "bsuid": { + "type": "string" + }, + "username": { + "type": "string" + }, + "parentBsuid": { + "type": "string", + "description": "Parent business-scoped user ID (`CC.ENT.\u2026`), present only when WhatsApp reports one for this person. It identifies the person across the business portfolios enrolled in one parent account. It cannot be used to send messages or block people; use `id` or `bsuid`.", + "example": "US.ENT.11815799212886844830" + } + }, + "required": ["id"] + }, + "ContactUpdatePayload": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "bsuid": { + "type": "string" + }, + "phoneNumber": { + "type": "string" + }, + "username": { + "type": "string" + }, + "parentBsuid": { + "type": "string", + "description": "Parent business-scoped user ID (`CC.ENT.\u2026`), present only when WhatsApp reports one for this person. It identifies the person across the business portfolios enrolled in one parent account. It cannot be used to send messages or block people; use `id` or `bsuid`.", + "example": "US.ENT.11815799212886844830" + }, + "identityChange": { + "$ref": "#/components/schemas/ContactIdentityChange" + }, + "fullName": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "pushName": { + "type": "string" + }, + "oldPushName": { + "type": "string" + }, + "businessName": { + "type": "string" + }, + "oldBusinessName": { + "type": "string" + }, + "pictureId": { + "type": "string" + }, + "pictureRemoved": { + "type": "boolean" + } + }, + "required": ["id"] + }, + "ContactIdentityChange": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "enum": ["phone_number_changed", "user_id_changed"], + "description": "`phone_number_changed`: the person changed their phone number, which also gives them a new business-scoped user ID. `user_id_changed`: their business-scoped user ID changed and WhatsApp did not share a phone number." + }, + "messageId": { + "type": "string", + "description": "WhatsApp's ID for the notification that reported the change." + }, + "previous": { + "$ref": "#/components/schemas/ContactPreviousIdentity" + } + }, + "required": ["reason", "messageId"], + "description": "Present when WhatsApp reported that the person's identifiers changed. The event's own identity fields describe the new identity." + }, + "ContactPreviousIdentity": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "bsuid": { + "type": "string" + }, + "parentBsuid": { + "type": "string" + } + }, + "description": "The identity the person had before the change. `id` is the contact key of that earlier identity." + }, + "PublicError": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "invalid_request_error", + "authentication_error", + "permission_error", + "conflict_error", + "rate_limit_error", + "api_error", + "upstream_error" + ] + }, + "code": { + "type": "string", + "enum": [ + "invalid_parameter", + "invalid_credential", + "entitlement_required", + "cloud_template_authority_restricted", + "gate_limit_reached", + "method_not_allowed", + "permission_denied", + "missing_scope", + "resource_not_found", + "hms_not_enabled", + "media_fetch_failed", + "resource_gone", + "feature_unavailable", + "stream_cursor_invalid", + "stream_cursor_expired", + "stream_connection_limit_reached", + "state_conflict", + "team_deletion_scheduled", + "hybrid_routing_paused", + "hybrid_choice_required", + "hybrid_transition_ineligible", + "hybrid_automation_unavailable", + "hybrid_transport_unavailable", + "hybrid_target_reference_unavailable", + "send_outcome_unknown", + "idempotency_conflict", + "idempotency_in_progress", + "idempotency_completed", + "idempotency_outcome_unknown", + "call_claimed", + "call_not_ringing", + "connection_limit", + "unsupported_for_connection", + "call_permission_required", + "invalid_sip_trunk", + "calls_disabled", + "call_recipient_opted_out", + "call_destination_blocked", + "call_permission_request_limited", + "call_permission_granted", + "order_status_transition_invalid", + "order_cancellation_failed", + "call_opt_out_limit", + "number_restricted", + "sip_trunk_in_use", + "sip_trunk_revision_conflict", + "sip_trunk_limit", + "sip_unavailable", + "voice_not_enabled", + "provider_credential_invalid", + "provider_unavailable", + "asset_not_ready", + "voice_asset_in_use", + "voice_asset_revision_conflict", + "voice_unavailable", + "rate_limit_exceeded", + "internal_error", + "operation_not_supported", + "upstream_failure", + "service_unavailable", + "credential_verification_unavailable", + "session_not_ready", + "command_dispatch_failed", + "result_unknown", + "parent_bsuid_not_supported", + "recipient_not_on_whatsapp", + "conversation_window_closed", + "template_not_approved", + "media_too_large", + "whatsapp_rate_limited", + "new_chat_limit_reached", + "whatsapp_account_restricted", + "whatsapp_groups_ineligible", + "whatsapp_group_creation_paused", + "whatsapp_group_limit_reached", + "whatsapp_group_full", + "whatsapp_group_suspended", + "whatsapp_group_has_no_participants", + "bansafe_suspended", + "bansafe_org_suspended", + "bansafe_cold_blocked", + "bansafe_cold_held", + "bansafe_throttled", + "bansafe_daily_allowance_reached", + "bansafe_accounting_unavailable", + "bansafe_send_outcome_unknown", + "campaigns_not_entitled", + "campaign_throughput_capped", + "campaign_approval_required", + "campaign_approval_stale", + "campaign_variables_missing" + ] + }, + "message": { + "type": "string" + }, + "param": { + "type": "string", + "nullable": true + }, + "request_id": { + "type": "string", + "description": "Matches the X-Request-Id response header. Include it when you contact support." + }, + "request_log_url": { + "type": "string", + "format": "uri", + "description": "Console page that shows this request. Returned only to organization and project credentials whose request was logged." + } + }, + "required": ["type", "code", "message", "param", "request_id"] + }, + "data": { + "type": "string", + "nullable": true, + "enum": [null], + "description": "Always null for a failed request." + }, + "docs": { + "type": "string", + "format": "uri" + } + }, + "required": ["error", "data", "docs"] + }, + "SuccessResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success"] + }, + "OrderPaymentUpdatedPayload": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["payment_status", "payment_method_selected"], + "description": "`payment_status` when WhatsApp reports a payment status; `payment_method_selected` when the buyer confirmed a one-click payment method" + }, + "reportedBy": { + "type": "string", + "enum": ["whatsapp"], + "description": "Polymorfa relays this report; it does not process or confirm funds" + }, + "providerEventId": { + "type": "string", + "description": "WhatsApp's notification or message ID" + }, + "referenceId": { + "type": "string", + "description": "Your order reference ID from the order details message", + "example": "order-1522" + }, + "conversation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "phoneNumber": { + "type": "string" + }, + "bsuid": { + "type": "string" + }, + "parentBsuid": { + "type": "string", + "description": "Parent business-scoped user ID (`CC.ENT.\u2026`), present only when WhatsApp reports one for this person. It identifies the person across the business portfolios enrolled in one parent account. It cannot be used to send messages or block people; use `id` or `bsuid`.", + "example": "US.ENT.11815799212886844830" + } + }, + "description": "The buyer" + }, + "status": { + "type": "string", + "description": "Payment status as WhatsApp reported it", + "example": "captured" + }, + "amount": { + "type": "object", + "properties": { + "value": { + "type": "integer" + }, + "offset": { + "type": "integer" + } + }, + "required": ["value", "offset"], + "description": "Reported amount; value divided by offset is the amount in currency units" + }, + "currency": { + "type": "string", + "example": "BRL" + }, + "transaction": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerTransactionId": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "status": { + "type": "string" + }, + "method": { + "type": "string" + }, + "errorCode": { + "type": "string" + } + }, + "description": "Payment provider transaction as WhatsApp reported it" + }, + "messageId": { + "type": "string", + "description": "Message that carried a payment method confirmation" + }, + "paymentMethod": { + "type": "string", + "example": "offsite_card_pay" + }, + "lastFourDigits": { + "type": "string" + }, + "credentialId": { + "type": "string", + "description": "Your credential ID from the order's payment settings" + }, + "paymentTimestamp": { + "type": "integer", + "description": "Unix time in seconds reported by WhatsApp" + } + }, + "required": [ + "kind", + "reportedBy", + "providerEventId", + "referenceId", + "conversation" + ] + }, + "CallEndedPayload": { + "type": "object", + "properties": { + "from": { + "type": "object", + "nullable": true, + "properties": { + "id": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "phoneNumber": { + "type": "string" + }, + "bsuid": { + "type": "string" + }, + "username": { + "type": "string" + }, + "parentBsuid": { + "type": "string", + "description": "Parent business-scoped user ID (`CC.ENT.\u2026`), present only when WhatsApp reports one for this person. It identifies the person across the business portfolios enrolled in one parent account. It cannot be used to send messages or block people; use `id` or `bsuid`.", + "example": "US.ENT.11815799212886844830" + } + }, + "required": ["id"] + }, + "callId": { + "type": "string" + }, + "durationSeconds": { + "type": "integer", + "description": "Total connected duration in seconds (0 if never answered).", + "example": 42 + }, + "reason": { + "type": "string", + "description": "Termination reason: user_hangup | timeout | lost_connection | rejected | capacity | pod_lost | connection_failed | upstream_failed | call_restricted (WhatsApp refused the call because the number is restricted to calling people it has already chatted with).", + "example": "user_hangup" + }, + "direction": { + "type": "string", + "enum": ["inbound", "outbound"], + "description": "Call direction.", + "example": "inbound" + }, + "hadVideo": { + "type": "boolean", + "description": "Whether the call carried video.", + "example": false + }, + "sessionConnection": { + "type": "string", + "enum": ["linked_device", "cloud_api"], + "description": "Connection of the session that carries the call: a linked device or the WhatsApp Business Cloud API.", + "example": "linked_device" + } + }, + "required": [ + "from", + "callId", + "durationSeconds", + "reason", + "direction", + "hadVideo" + ] + }, + "ConversationRef": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Opaque Polymorfa resource ID. Keep this value as a string.", + "example": "739182640518203" + }, + "phoneNumber": { + "type": "string" + }, + "bsuid": { + "type": "string" + }, + "username": { + "type": "string" + }, + "parentBsuid": { + "type": "string", + "description": "Parent business-scoped user ID (`CC.ENT.\u2026`), present only when WhatsApp reports one for this person. It identifies the person across the business portfolios enrolled in one parent account. It cannot be used to send messages or block people; use `id` or `bsuid`.", + "example": "US.ENT.11815799212886844830" + }, + "sender": { + "$ref": "#/components/schemas/IdentityRef" + } + }, + "required": ["id"] + } + } + } +} diff --git a/contracts/official-api-round2/oa-21.json b/contracts/official-api-round2/oa-21.json new file mode 100644 index 00000000..5ba8b3f4 --- /dev/null +++ b/contracts/official-api-round2/oa-21.json @@ -0,0 +1,499 @@ +{ + "paths": { + "/messaging/{session}/official-groups/{group}/picture": { + "put": { + "operationId": "setOfficialGroupPicture", + "tags": ["Official groups"], + "summary": "Set an Official group picture", + "description": "**Protocols:** Meta Cloud API\n\n**Required scope:** `groups:manage`\n\nReplaces the group's profile picture. Send `multipart/form-data` with one `file` part: a square JPEG of at least 192 x 192 pixels and at most 5 MB. A `group.update` event with action `settings_updated` reports whether WhatsApp applied it. Repeating the same upload is safe. Beta: requires team enrollment and a Number with an Official API connection whose WhatsApp Business Account is an Official Business Account.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "groups:manage", + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "description": "Number ID." + }, + "required": true, + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[1-9][0-9]{0,18}$", + "description": "Conversation ID of the group.", + "example": "739182640518203" + }, + "required": true, + "name": "group", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/SetOfficialGroupPictureRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetOfficialGroupPictureResponse" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "400": { + "description": "A parameter is invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "The credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "402": { + "description": "Account or billing restrictions prevent the request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "403": { + "description": "The credential lacks the scope, the team is not enrolled in the beta (`feature_unavailable`), the number is not eligible for groups (`whatsapp_groups_ineligible`), group creation is paused (`whatsapp_group_creation_paused`), or the group is suspended (`whatsapp_group_suspended`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "404": { + "description": "The Number or group is unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "409": { + "description": "The Number has no Official API connection (`unsupported_for_connection`), or a WhatsApp group limit was reached (`whatsapp_group_limit_reached`, `whatsapp_group_full`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "413": { + "description": "The picture is larger than 5 MB (`media_too_large`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "429": { + "description": "WhatsApp is limiting group requests from this number (`whatsapp_rate_limited`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "502": { + "description": "WhatsApp's response could not be read.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "503": { + "description": "A required service is unavailable. For a change, WhatsApp may have applied it: read the group before repeating it.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "504": { + "description": "The reply deadline elapsed after dispatch (`result_unknown`). The picture may have changed; read the group before repeating the upload.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicError" + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + } + }, + "x-mint": { + "content": "Meta Cloud API groups:manage" + }, + "x-protocols": ["Meta Cloud API"] + } + } + }, + "components": { + "schemas": { + "PublicError": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "invalid_request_error", + "authentication_error", + "permission_error", + "conflict_error", + "rate_limit_error", + "api_error", + "upstream_error" + ] + }, + "code": { + "type": "string", + "enum": [ + "invalid_parameter", + "invalid_credential", + "entitlement_required", + "cloud_template_authority_restricted", + "gate_limit_reached", + "method_not_allowed", + "permission_denied", + "missing_scope", + "resource_not_found", + "hms_not_enabled", + "media_fetch_failed", + "resource_gone", + "feature_unavailable", + "stream_cursor_invalid", + "stream_cursor_expired", + "stream_connection_limit_reached", + "state_conflict", + "team_deletion_scheduled", + "hybrid_routing_paused", + "hybrid_choice_required", + "hybrid_transition_ineligible", + "hybrid_automation_unavailable", + "hybrid_group_linked_devices_unavailable", + "hybrid_transport_unavailable", + "hybrid_target_reference_unavailable", + "send_outcome_unknown", + "idempotency_conflict", + "idempotency_in_progress", + "idempotency_completed", + "idempotency_outcome_unknown", + "call_claimed", + "call_not_ringing", + "connection_limit", + "unsupported_for_connection", + "call_permission_required", + "invalid_sip_trunk", + "calls_disabled", + "call_recipient_opted_out", + "call_destination_blocked", + "call_permission_request_limited", + "call_permission_granted", + "order_status_transition_invalid", + "order_cancellation_failed", + "call_opt_out_limit", + "number_restricted", + "sip_trunk_in_use", + "sip_trunk_revision_conflict", + "sip_trunk_limit", + "sip_unavailable", + "voice_not_enabled", + "provider_credential_invalid", + "provider_unavailable", + "asset_not_ready", + "voice_asset_in_use", + "voice_asset_revision_conflict", + "voice_unavailable", + "rate_limit_exceeded", + "internal_error", + "operation_not_supported", + "upstream_failure", + "service_unavailable", + "credential_verification_unavailable", + "session_not_ready", + "command_dispatch_failed", + "result_unknown", + "parent_bsuid_not_supported", + "recipient_not_on_whatsapp", + "conversation_window_closed", + "template_not_approved", + "media_too_large", + "whatsapp_rate_limited", + "new_chat_limit_reached", + "marketing_disabled_on_cloud_api", + "whatsapp_account_restricted", + "whatsapp_authorization_required", + "whatsapp_payment_method_required", + "whatsapp_number_not_registered", + "whatsapp_groups_ineligible", + "whatsapp_group_creation_paused", + "whatsapp_group_limit_reached", + "whatsapp_group_full", + "whatsapp_group_suspended", + "whatsapp_group_has_no_participants", + "bansafe_suspended", + "bansafe_org_suspended", + "bansafe_cold_blocked", + "bansafe_cold_held", + "bansafe_throttled", + "bansafe_daily_allowance_reached", + "bansafe_accounting_unavailable", + "bansafe_send_outcome_unknown", + "campaigns_not_entitled", + "campaign_throughput_capped", + "campaign_approval_required", + "campaign_approval_stale", + "campaign_variables_missing" + ] + }, + "message": { + "type": "string" + }, + "param": { + "type": "string", + "nullable": true + }, + "request_id": { + "type": "string", + "description": "Matches the X-Request-Id response header. Include it when you contact support." + }, + "request_log_url": { + "type": "string", + "format": "uri", + "description": "Console page that shows this request. Returned only to organization and project credentials whose request was logged." + } + }, + "required": ["type", "code", "message", "param", "request_id"] + }, + "data": { + "type": "string", + "nullable": true, + "enum": [null], + "description": "Always null for a failed request." + }, + "docs": { + "type": "string", + "format": "uri" + } + }, + "required": ["error", "data", "docs"] + }, + "SetOfficialGroupPictureResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + }, + "data": { + "$ref": "#/components/schemas/OfficialGroupChangeAccepted" + } + }, + "required": ["success", "data"] + }, + "OfficialGroupChangeAccepted": { + "type": "object", + "properties": { + "accepted": { + "type": "boolean", + "enum": [true], + "description": "WhatsApp accepted the request. Its outcome arrives as a `group.update` or `group.participant` event." + } + }, + "required": ["accepted"] + }, + "SetOfficialGroupPictureRequest": { + "type": "object", + "properties": { + "file": { + "type": "string", + "description": "The picture: a square JPEG of at least 192 x 192 pixels and at most 5 MB.", + "format": "binary" + } + }, + "required": ["file"] + } + } + } +} diff --git a/contracts/official-api-round2/oa-30.json b/contracts/official-api-round2/oa-30.json new file mode 100644 index 00000000..5267b681 --- /dev/null +++ b/contracts/official-api-round2/oa-30.json @@ -0,0 +1,2384 @@ +{ + "paths": { + "/platform/webhooks": { + "get": { + "operationId": "listOrganizationWebhooks", + "tags": ["webhooks"], + "summary": "List team webhooks", + "description": "Lists only team-owned webhook endpoints. Project-owned endpoints are excluded.\n\nAPI keys require `webhooks:read`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "eventType", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + { + "name": "enabled", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationWebhook" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": ["data", "page"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + }, + "post": { + "operationId": "createOrganizationWebhook", + "tags": ["webhooks"], + "summary": "Create an team webhook", + "description": "Creates an team-owned webhook and returns its generated signing secret once.\n\nAPI keys require `webhooks:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationWebhookInput" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookCreationReceipt" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/webhooks/{webhookId}": { + "get": { + "operationId": "getOrganizationWebhook", + "tags": ["webhooks"], + "summary": "Get an team webhook", + "description": "Returns one team-owned webhook without secret values.\n\nAPI keys require `webhooks:read`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhook" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "patch": { + "operationId": "updateOrganizationWebhook", + "tags": ["webhooks"], + "summary": "Update an team webhook", + "description": "Updates settings without rotating the signing secret.\n\nAPI keys require `webhooks:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationWebhookInput" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookMutationReceipt" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + }, + "delete": { + "operationId": "deleteOrganizationWebhook", + "tags": ["webhooks"], + "summary": "Delete an team webhook", + "description": "Deletes one team-owned webhook.\n\nAPI keys require `webhooks:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookDeletionReceipt" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/webhooks/{webhookId}/secret-rotations": { + "post": { + "operationId": "rotateOrganizationWebhookSecret", + "tags": ["webhooks"], + "summary": "Rotate an team webhook secret", + "description": "Rotates the signing secret and returns the new secret once.\n\nAPI keys require `webhooks:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RotateWebhookSecretInput" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookSecretRotationReceipt" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/webhooks/{webhookId}/tests": { + "post": { + "operationId": "testOrganizationWebhook", + "tags": ["webhooks"], + "summary": "Send a team webhook test", + "description": "Creates a synthetic team event and a traceable webhook delivery.\n\nAPI keys require `webhooks:manage`. Project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestOrganizationWebhookInput" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookTestReceipt" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/projects/{projectId}/webhooks": { + "get": { + "operationId": "listProjectWebhooks", + "tags": ["webhooks"], + "summary": "List project webhooks", + "description": "Lists webhooks owned by the exact project in the route.\n\nRequires `webhooks:read`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "eventType", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + { + "name": "enabled", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectWebhook" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": ["data", "page"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "post": { + "operationId": "createProjectWebhook", + "tags": ["webhooks"], + "summary": "Create a project webhook", + "description": "Creates a project-owned webhook and returns its generated signing secret once.\n\nRequires `webhooks:manage`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProjectWebhookInput" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookCreationReceipt" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/projects/{projectId}/webhooks/{webhookId}": { + "get": { + "operationId": "getProjectWebhook", + "tags": ["webhooks"], + "summary": "Get a project webhook", + "description": "Returns one webhook owned by the exact project without secret values.\n\nRequires `webhooks:read`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:read", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhook" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "patch": { + "operationId": "updateProjectWebhook", + "tags": ["webhooks"], + "summary": "Update a project webhook", + "description": "Updates settings without rotating the signing secret.\n\nRequires `webhooks:manage`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProjectWebhookInput" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookMutationReceipt" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + }, + "delete": { + "operationId": "deleteProjectWebhook", + "tags": ["webhooks"], + "summary": "Delete a project webhook", + "description": "Deletes one webhook owned by the exact project.\n\nRequires `webhooks:manage`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookDeletionReceipt" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/projects/{projectId}/webhooks/{webhookId}/secret-rotations": { + "post": { + "operationId": "rotateProjectWebhookSecret", + "tags": ["webhooks"], + "summary": "Rotate a project webhook secret", + "description": "Rotates the signing secret and returns the new secret once.\n\nRequires `webhooks:manage`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RotateWebhookSecretInput" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookSecretRotationReceipt" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + } + } + } + }, + "/platform/projects/{projectId}/webhooks/{webhookId}/tests": { + "post": { + "operationId": "testProjectWebhook", + "tags": ["webhooks"], + "summary": "Send a project webhook test", + "description": "Creates a synthetic project event and a traceable webhook delivery. An optional complete native envelope is accepted only with an exact HMS-enabled project session.\n\nRequires `webhooks:manage`. Project tokens only access resources in their own project.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestProjectWebhookInput" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "X-Trace-Id": { + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" + }, + "description": "W3C trace ID of this request. Your `traceparent` trace ID when you sent a valid one." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookTestReceipt" + } + }, + "required": ["data"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "422": { + "$ref": "#/components/responses/Unprocessable" + } + } + } + } + }, + "components": { + "schemas": { + "WebhookBatching": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "maxEvents": { + "type": "integer", + "minimum": 2, + "maximum": 100 + }, + "windowMs": { + "type": "integer", + "minimum": 100, + "maximum": 1000 + } + }, + "required": ["enabled", "maxEvents", "windowMs"] + }, + "WebhookBatchingInput": { + "type": "object", + "additionalProperties": false, + "description": "Opt-in native webhook conversation batching beta. On create, omission keeps single-event delivery. On PATCH, omission preserves the stored batching settings. Requires feature availability and enrollment. Events without replayable conversation payloads use single delivery.", + "properties": { + "enabled": { + "type": "boolean" + }, + "maxEvents": { + "type": "integer", + "minimum": 2, + "maximum": 100, + "default": 100 + }, + "windowMs": { + "type": "integer", + "minimum": 100, + "maximum": 1000, + "default": 1000 + } + }, + "required": ["enabled"] + }, + "WebhookEventBatch": { + "type": "object", + "additionalProperties": false, + "description": "Version 1 batch of existing native event objects. A retry keeps this ID, membership, order and JSON bytes. Acknowledge the whole request with 2xx; partial acknowledgement is unsupported.", + "properties": { + "object": { + "type": "string", + "enum": ["event_batch"] + }, + "version": { + "type": "integer", + "enum": [1] + }, + "id": { + "type": "string", + "format": "uuid" + }, + "events": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": true + } + } + }, + "required": ["object", "version", "id", "events"] + }, + "ProjectWebhookTestReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "operationId": { + "type": "string", + "format": "uuid" + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + }, + "eventId": { + "type": "string", + "format": "uuid" + }, + "deliveryId": { + "type": "string", + "format": "uuid" + } + }, + "required": ["eventId", "deliveryId", "operationId", "idempotency"] + }, + "IdempotencyReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "key": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "replayed": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "expiresAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["id", "key", "replayed", "createdAt", "expiresAt"] + }, + "TestProjectWebhookInput": { + "description": "Names reserved for internal use are rejected with HTTP 400, whether supplied as eventType or in the native event body, before a test event or delivery is created.", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "eventType": { + "type": "string", + "default": "polymorfa.test", + "pattern": "^[a-z0-9][a-z0-9_.-]{0,127}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "eventType": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9_.-]{0,127}$" + }, + "body": { + "$ref": "#/components/schemas/EncodedEventPayload" + }, + "sessionId": { + "type": "string", + "format": "uuid" + } + }, + "required": ["body", "sessionId"] + } + ] + }, + "EncodedEventPayload": { + "type": "object", + "additionalProperties": false, + "properties": { + "encoding": { + "type": "string", + "enum": ["base64"] + }, + "contentType": { + "type": "string", + "enum": ["application/json"] + }, + "data": { + "type": "string", + "format": "byte", + "maxLength": 87384, + "description": "Canonical base64 for the complete native webhook body; decoded bytes are limited to 65,536 bytes." + } + }, + "required": ["encoding", "contentType", "data"] + }, + "ProjectWebhookSecretRotationReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "webhookId": { + "type": "string", + "format": "uuid" + }, + "operationId": { + "type": "string", + "nullable": true, + "enum": [null] + }, + "secret": { + "type": "string", + "nullable": true, + "writeOnly": true + }, + "secretAvailable": { + "type": "boolean" + }, + "secretMetadata": { + "$ref": "#/components/schemas/WebhookSigningSecretMetadata" + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + } + }, + "required": [ + "webhookId", + "operationId", + "secret", + "secretAvailable", + "secretMetadata", + "idempotency" + ] + }, + "WebhookSigningSecretMetadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "version": { + "type": "integer", + "minimum": 1 + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "previousValidUntil": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "required": ["version", "createdAt", "previousValidUntil"] + }, + "RotateWebhookSecretInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "overlapSeconds": { + "type": "integer", + "minimum": 0, + "maximum": 86400, + "default": 3600 + } + } + }, + "ProjectWebhookDeletionReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "webhookId": { + "type": "string", + "format": "uuid" + }, + "deleted": { + "type": "boolean", + "enum": [true] + }, + "operationId": { + "type": "string", + "nullable": true, + "enum": [null] + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + } + }, + "required": ["webhookId", "deleted", "operationId", "idempotency"] + }, + "ProjectWebhookMutationReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "webhook": { + "$ref": "#/components/schemas/ProjectWebhook" + }, + "operationId": { + "type": "string", + "nullable": true, + "enum": [null] + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + } + }, + "required": ["webhook", "operationId", "idempotency"] + }, + "ProjectWebhook": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "owner": { + "type": "string", + "enum": ["project"] + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "url": { + "type": "string", + "format": "uri", + "maxLength": 2048 + }, + "eventTypes": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string", + "enum": ["native", "meta"] + }, + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicy" + }, + "batching": { + "$ref": "#/components/schemas/WebhookBatching" + }, + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderMetadata" + } + }, + "secret": { + "$ref": "#/components/schemas/WebhookSigningSecretMetadata" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "organizationId", + "owner", + "projectId", + "url", + "eventTypes", + "enabled", + "format", + "retryPolicy", + "batching", + "headers", + "secret", + "createdAt", + "updatedAt" + ] + }, + "WebhookHeaderMetadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": ["name"] + }, + "WebhookRetryPolicy": { + "type": "object", + "additionalProperties": false, + "properties": { + "maximumAttempts": { + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "backoff": { + "type": "string", + "enum": ["constant", "linear", "exponential"] + }, + "initialDelaySeconds": { + "type": "integer", + "minimum": 1, + "maximum": 3600 + } + }, + "required": ["maximumAttempts", "backoff", "initialDelaySeconds"] + }, + "UpdateProjectWebhookInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2048 + }, + "eventTypes": { + "description": "Names reserved for internal use cannot be newly subscribed. Existing stored entries may be preserved during updates but never match deliveries.", + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string", + "enum": ["native", "meta"] + }, + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicyInput" + }, + "batching": { + "$ref": "#/components/schemas/WebhookBatchingInput" + }, + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderInput" + } + } + }, + "minProperties": 1 + }, + "WebhookHeaderInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$" + }, + "value": { + "type": "string", + "maxLength": 4096, + "pattern": "^(?![\\s\\S]*[\\u0000-\\u0008\\u000A-\\u001F\\u007F])[\\s\\S]*$", + "x-polymorfa-max-utf8-bytes": 4096, + "writeOnly": true, + "description": "Encrypted write-only HTTP field value. Limited to 4096 UTF-8 bytes. Line breaks, NUL, and other control characters except horizontal tab are rejected." + } + }, + "required": ["name", "value"] + }, + "WebhookRetryPolicyInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "maximumAttempts": { + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "backoff": { + "type": "string", + "enum": ["constant", "linear", "exponential"] + }, + "initialDelaySeconds": { + "type": "integer", + "minimum": 1, + "maximum": 3600 + } + }, + "required": ["maximumAttempts", "backoff", "initialDelaySeconds"] + }, + "ProjectWebhookCreationReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "webhook": { + "$ref": "#/components/schemas/ProjectWebhook" + }, + "operationId": { + "type": "string", + "nullable": true, + "enum": [null] + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + }, + "secret": { + "type": "string", + "nullable": true, + "writeOnly": true + }, + "secretAvailable": { + "type": "boolean" + } + }, + "required": [ + "webhook", + "operationId", + "idempotency", + "secret", + "secretAvailable" + ] + }, + "CreateProjectWebhookInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2048 + }, + "eventTypes": { + "description": "Names reserved for internal use cannot be newly subscribed. Existing stored entries may be preserved during updates but never match deliveries.", + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "enabled": { + "type": "boolean", + "default": true + }, + "format": { + "type": "string", + "enum": ["native", "meta"], + "default": "native" + }, + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicyInput" + }, + "batching": { + "$ref": "#/components/schemas/WebhookBatchingInput" + }, + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderInput" + } + } + }, + "required": ["url", "eventTypes"] + }, + "CursorPageMetadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "nextCursor": { + "type": "string", + "nullable": true + }, + "hasMore": { + "type": "boolean" + } + }, + "required": ["nextCursor", "hasMore"] + }, + "OrganizationWebhookTestReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "operationId": { + "type": "string", + "format": "uuid" + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + }, + "eventId": { + "type": "string", + "format": "uuid" + }, + "deliveryId": { + "type": "string", + "format": "uuid" + } + }, + "required": ["eventId", "deliveryId", "operationId", "idempotency"] + }, + "TestOrganizationWebhookInput": { + "type": "object", + "additionalProperties": false, + "description": "A team webhook test accepts an optional event type. Supplied event bodies and session IDs are rejected with HTTP 400.", + "properties": { + "eventType": { + "type": "string", + "default": "polymorfa.test", + "pattern": "^[a-z0-9][a-z0-9_.-]{0,127}$" + } + } + }, + "OrganizationWebhookSecretRotationReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "webhookId": { + "type": "string", + "format": "uuid" + }, + "operationId": { + "type": "string", + "nullable": true, + "enum": [null] + }, + "secret": { + "type": "string", + "nullable": true, + "writeOnly": true + }, + "secretAvailable": { + "type": "boolean" + }, + "secretMetadata": { + "$ref": "#/components/schemas/WebhookSigningSecretMetadata" + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + } + }, + "required": [ + "webhookId", + "operationId", + "secret", + "secretAvailable", + "secretMetadata", + "idempotency" + ] + }, + "OrganizationWebhookDeletionReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "webhookId": { + "type": "string", + "format": "uuid" + }, + "deleted": { + "type": "boolean", + "enum": [true] + }, + "operationId": { + "type": "string", + "nullable": true, + "enum": [null] + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + } + }, + "required": ["webhookId", "deleted", "operationId", "idempotency"] + }, + "OrganizationWebhookMutationReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "webhook": { + "$ref": "#/components/schemas/OrganizationWebhook" + }, + "operationId": { + "type": "string", + "nullable": true, + "enum": [null] + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + } + }, + "required": ["webhook", "operationId", "idempotency"] + }, + "OrganizationWebhook": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "owner": { + "type": "string", + "enum": ["organization"] + }, + "projectId": { + "type": "string", + "nullable": true, + "enum": [null] + }, + "url": { + "type": "string", + "format": "uri", + "maxLength": 2048 + }, + "eventTypes": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string", + "enum": ["native", "meta"] + }, + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicy" + }, + "batching": { + "$ref": "#/components/schemas/WebhookBatching" + }, + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderMetadata" + } + }, + "secret": { + "$ref": "#/components/schemas/WebhookSigningSecretMetadata" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "organizationId", + "owner", + "projectId", + "url", + "eventTypes", + "enabled", + "format", + "retryPolicy", + "batching", + "headers", + "secret", + "createdAt", + "updatedAt" + ] + }, + "UpdateOrganizationWebhookInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2048 + }, + "eventTypes": { + "description": "Names reserved for internal use cannot be newly subscribed. Existing stored entries may be preserved during updates but never match deliveries.", + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string", + "enum": ["native", "meta"] + }, + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicyInput" + }, + "batching": { + "$ref": "#/components/schemas/WebhookBatchingInput" + }, + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderInput" + } + } + }, + "minProperties": 1 + }, + "OrganizationWebhookCreationReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "webhook": { + "$ref": "#/components/schemas/OrganizationWebhook" + }, + "operationId": { + "type": "string", + "nullable": true, + "enum": [null] + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + }, + "secret": { + "type": "string", + "nullable": true, + "writeOnly": true + }, + "secretAvailable": { + "type": "boolean" + } + }, + "required": [ + "webhook", + "operationId", + "idempotency", + "secret", + "secretAvailable" + ] + }, + "CreateOrganizationWebhookInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2048 + }, + "eventTypes": { + "description": "Names reserved for internal use cannot be newly subscribed. Existing stored entries may be preserved during updates but never match deliveries.", + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "enabled": { + "type": "boolean", + "default": true + }, + "format": { + "type": "string", + "enum": ["native", "meta"], + "default": "native" + }, + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicyInput" + }, + "batching": { + "$ref": "#/components/schemas/WebhookBatchingInput" + }, + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderInput" + } + } + }, + "required": ["url", "eventTypes"] + } + } + } +} diff --git a/docs/calls-simultaneous-calls.md b/docs/calls-simultaneous-calls.md new file mode 100644 index 00000000..1ff677ac --- /dev/null +++ b/docs/calls-simultaneous-calls.md @@ -0,0 +1,24 @@ +# Simultaneous calls + +## Simultaneous calls source preparation + +This counterpart is prepared against API #559 merge `c1a43a1e`. It is not a +registry availability claim. Use the HTTP API until a matching SDK release is +published and accepted against the target environment. + +Call-settings reads expose `simultaneousCalls`, default false. Updates accept +`simultaneousCalls` and `acknowledgeRisk`. Omission preserves stored state; +explicit false disables the setting. Acknowledgment alone changes no setting +and is rejected. The API requires a team key and `acknowledgeRisk: true` for an +off-to-on transition. Project tokens may disable it. The SDK forwards values +without guessing the stored state. Existing `invalid_parameter`, +`permission_denied` and `state_conflict` errors remain authoritative. + +The setting affects linked-device Numbers. Cloud API Numbers ignore it. +Unlimited concurrent calls differ from WhatsApp's apps and void Ban Insurance +when the 30-day claim window overlaps an enabled period. Claim verdicts and +`bansafe.claim` webhook verdicts recognize `simultaneous_calls`. Claim evidence +includes `simultaneousCalls`; the webhook does not acquire an evidence object. + +No browser/client-token authority, call media behavior, automatic state read, +retry policy or feature availability is added by these SDK fields. diff --git a/docs/demos/2026-10-18/README.md b/docs/demos/2026-10-18/README.md new file mode 100644 index 00000000..f93c47dd --- /dev/null +++ b/docs/demos/2026-10-18/README.md @@ -0,0 +1,22 @@ +# Custom UI and Inspector demos + +Each recording shows a custom application beside its paired Inspector WhatsApp +view. The application sends through its native server SDK, receives an +independently typed reply through a signed HTTPS webhook and reloads the +conversation. These are concurrent captures of working applications. + +- [Go: net/http and templ](go-round-trip.mp4) +- [Python: FastAPI and Jinja](python-round-trip.mp4) +- [PHP: Laravel and Blade](php-round-trip.mp4) +- [Rust: Yew and Axum](rust-round-trip.mp4) +- [C#: ASP.NET Core](csharp-round-trip.mp4) + +The runs use isolated local test numbers and simulated WhatsApp with a local +backend candidate. The native pane is sampled at about two frames per second. +No real WhatsApp recipient, customer data or authentication screen appears. +The PHP clip retains an idle wait and conversation reselection after reload. + +One canonical desktop UI per language is recorded. Python's Django and PHP's +Symfony handlers also have HTTP checks; C# Blazor and MAUI have separate +acceptance limits. These videos do not establish registry publication, hosted +API acceptance or production availability. diff --git a/docs/demos/2026-10-18/csharp-round-trip.mp4 b/docs/demos/2026-10-18/csharp-round-trip.mp4 new file mode 100644 index 00000000..b29fe8ce Binary files /dev/null and b/docs/demos/2026-10-18/csharp-round-trip.mp4 differ diff --git a/docs/demos/2026-10-18/go-round-trip.mp4 b/docs/demos/2026-10-18/go-round-trip.mp4 new file mode 100644 index 00000000..30e9f478 Binary files /dev/null and b/docs/demos/2026-10-18/go-round-trip.mp4 differ diff --git a/docs/demos/2026-10-18/php-round-trip.mp4 b/docs/demos/2026-10-18/php-round-trip.mp4 new file mode 100644 index 00000000..7add2486 Binary files /dev/null and b/docs/demos/2026-10-18/php-round-trip.mp4 differ diff --git a/docs/demos/2026-10-18/python-round-trip.mp4 b/docs/demos/2026-10-18/python-round-trip.mp4 new file mode 100644 index 00000000..fafeea31 Binary files /dev/null and b/docs/demos/2026-10-18/python-round-trip.mp4 differ diff --git a/docs/demos/2026-10-18/rust-round-trip.mp4 b/docs/demos/2026-10-18/rust-round-trip.mp4 new file mode 100644 index 00000000..cb680592 Binary files /dev/null and b/docs/demos/2026-10-18/rust-round-trip.mp4 differ diff --git a/docs/images/native-sdk-examples/README.md b/docs/images/native-sdk-examples/README.md new file mode 100644 index 00000000..98bf63ce --- /dev/null +++ b/docs/images/native-sdk-examples/README.md @@ -0,0 +1,59 @@ +# Native SDK example browser evidence + +Tested SDK source: `a01ff3e2ec39cc243a3c81f507b839494ccf484c`. +The screenshots render the actual example hosts and bundled Web Components in +the T3 collaborative browser. These are local fixtures, not published packages. + +| Example | Desktop viewport | Narrow viewport | Result | +| ------------------------- | ---------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Python FastAPI and Django | 1440 x 1000 | 390 x 844 | Browser grant and empty inbox render; logout removes components and token access returns 401. | +| Go net/http and Gin | 1440 x 1000 | 390 x 844 | Same browser grant, empty inbox and logout results. | +| PHP Laravel and Symfony | 1440 x 1000 | 390 x 844 | Same results after the HTMX response settles. | +| Rust Yew with Axum | 1440 x 1000 | 390 x 844 | Real WASM form sign-in, grant, empty inbox and logout succeed. | +| ASP.NET Core | 1440 x 640 | 390 x 844 | Grant and empty inbox render; screenshots scroll past the authentication form. | +| Blazor WebAssembly | 1440 x 1000 | 390 x 844 | Same authenticated ASP.NET server, grant and empty inbox. Separate 1440 x 640 screenshot shows Reconnecting and Retry when the synthetic token service refuses minting. | + +All measured document widths fit their viewports. Screenshots can have a +different pixel size because the browser scales its capture; `manifest.json` +records both CSS viewport and PNG dimensions and SHA-256 hashes. + +## Fixture limits + +- One synthetic application identity and an assigned `authorized-support` + session. Server SDKs call a loopback token service; the history source is the + examples' real application-owned empty store. No customer account is used. +- Connected describes the short-lived browser grant. It does not establish a + WhatsApp connection, customer-owned Signal custody, hosted retention, + supplementary QuickLink execution or browser-tunnel acceptance. +- No message was sent and no pairing invitation was exercised. The local + upstream checks the API version and server-selected token fields. Real + authentication, live membership, enrollment and funding remain API concerns. +- Authentication forms, credentials and customer content are excluded from the + committed screenshots. ASP.NET logout returns 204 and later token access 401; + Blazor shares that server session. No desktop logout UI acceptance is claimed. +- MAUI Mac Catalyst builds at this source with SDK/workload 10.0.401 and + warnings treated as errors. Native interaction remains blocked: the supplied + desktop driver returns `-10005: The element ID is no longer valid` for controls + immediately after a fresh accessibility snapshot. No post-sign-in native + screenshot is claimed. Windows, Android and iOS are outside the supplied host. + +The first navigation raced FastAPI startup (`ERR_CONNECTION_REFUSED`); retry +after server readiness passed. The first Rust fixture rejected its legitimate +server-generated UUID identity; correcting that fixture passed without an SDK +edit. The first ASP.NET fixture used an invalid synthetic team key; +the documented credential shape corrected it. Reusing a loopback origin also +cached Yew's static page: a cache reload and distinct URL selected ASP.NET and +passed. These setup failures are not SDK runtime regressions. + +## Build and source verification + +Node 24 rebuilds all five web asset entry points. Go builds the actual web host; +Rust stable builds the Yew release WASM and Axum server; .NET builds ASP.NET, +publishes/prepares Blazor and builds MAUI. The original Trunk invocation rejected +the inherited `NO_COLOR=1`; `NO_COLOR=true` passed. Dependencies and locks remain +unchanged. This commit adds only screenshots, this index and their manifest. + +The 579-operation pinned baseline and separately mapped supplements remain +distinct from final release coverage. API contract dates, publication holds, +versions and runtime logic are unchanged. Matching API acceptance, registry +publication, clean registry installs and deployed customer routes remain gates. diff --git a/docs/images/native-sdk-examples/aspnet-desktop.png b/docs/images/native-sdk-examples/aspnet-desktop.png new file mode 100644 index 00000000..755ad384 Binary files /dev/null and b/docs/images/native-sdk-examples/aspnet-desktop.png differ diff --git a/docs/images/native-sdk-examples/aspnet-phone.png b/docs/images/native-sdk-examples/aspnet-phone.png new file mode 100644 index 00000000..2ccab069 Binary files /dev/null and b/docs/images/native-sdk-examples/aspnet-phone.png differ diff --git a/docs/images/native-sdk-examples/blazor-desktop.png b/docs/images/native-sdk-examples/blazor-desktop.png new file mode 100644 index 00000000..59cdd20e Binary files /dev/null and b/docs/images/native-sdk-examples/blazor-desktop.png differ diff --git a/docs/images/native-sdk-examples/blazor-phone.png b/docs/images/native-sdk-examples/blazor-phone.png new file mode 100644 index 00000000..0e990c22 Binary files /dev/null and b/docs/images/native-sdk-examples/blazor-phone.png differ diff --git a/docs/images/native-sdk-examples/blazor-token-unavailable.png b/docs/images/native-sdk-examples/blazor-token-unavailable.png new file mode 100644 index 00000000..e1ea662d Binary files /dev/null and b/docs/images/native-sdk-examples/blazor-token-unavailable.png differ diff --git a/docs/images/native-sdk-examples/django-desktop.png b/docs/images/native-sdk-examples/django-desktop.png new file mode 100644 index 00000000..1c13680c Binary files /dev/null and b/docs/images/native-sdk-examples/django-desktop.png differ diff --git a/docs/images/native-sdk-examples/django-phone.png b/docs/images/native-sdk-examples/django-phone.png new file mode 100644 index 00000000..ea496c81 Binary files /dev/null and b/docs/images/native-sdk-examples/django-phone.png differ diff --git a/docs/images/native-sdk-examples/fastapi-desktop.png b/docs/images/native-sdk-examples/fastapi-desktop.png new file mode 100644 index 00000000..1c13680c Binary files /dev/null and b/docs/images/native-sdk-examples/fastapi-desktop.png differ diff --git a/docs/images/native-sdk-examples/fastapi-phone.png b/docs/images/native-sdk-examples/fastapi-phone.png new file mode 100644 index 00000000..ea496c81 Binary files /dev/null and b/docs/images/native-sdk-examples/fastapi-phone.png differ diff --git a/docs/images/native-sdk-examples/gin-desktop.png b/docs/images/native-sdk-examples/gin-desktop.png new file mode 100644 index 00000000..1c13680c Binary files /dev/null and b/docs/images/native-sdk-examples/gin-desktop.png differ diff --git a/docs/images/native-sdk-examples/gin-phone.png b/docs/images/native-sdk-examples/gin-phone.png new file mode 100644 index 00000000..ea496c81 Binary files /dev/null and b/docs/images/native-sdk-examples/gin-phone.png differ diff --git a/docs/images/native-sdk-examples/laravel-desktop.png b/docs/images/native-sdk-examples/laravel-desktop.png new file mode 100644 index 00000000..1c13680c Binary files /dev/null and b/docs/images/native-sdk-examples/laravel-desktop.png differ diff --git a/docs/images/native-sdk-examples/laravel-phone.png b/docs/images/native-sdk-examples/laravel-phone.png new file mode 100644 index 00000000..ea496c81 Binary files /dev/null and b/docs/images/native-sdk-examples/laravel-phone.png differ diff --git a/docs/images/native-sdk-examples/manifest.json b/docs/images/native-sdk-examples/manifest.json new file mode 100644 index 00000000..1c0a2569 --- /dev/null +++ b/docs/images/native-sdk-examples/manifest.json @@ -0,0 +1,314 @@ +{ + "testedSdk": "a01ff3e2ec39cc243a3c81f507b839494ccf484c", + "transport": "Synthetic loopback HTTP. No live WhatsApp, registry or pairing acceptance.", + "screenshots": [ + { + "host": "fastapi", + "state": "desktop", + "file": "fastapi-desktop.png", + "viewport": { + "width": 1440, + "height": 1000 + }, + "pngDimensions": [1280, 889], + "sha256": "0f14268265667bedf32a8a4fc898622ff3b8fcbefc997a6cc5c3861f08746264", + "noHorizontalOverflow": true + }, + { + "host": "fastapi", + "state": "phone", + "file": "fastapi-phone.png", + "viewport": { + "width": 390, + "height": 844 + }, + "pngDimensions": [780, 1688], + "sha256": "19fecc73ea32e63ff44ef8aaae847c7be6aff8f44f08aaea8fe59a57abc41b00", + "noHorizontalOverflow": true + }, + { + "host": "django", + "state": "desktop", + "file": "django-desktop.png", + "viewport": { + "width": 1440, + "height": 1000 + }, + "pngDimensions": [1280, 889], + "sha256": "0f14268265667bedf32a8a4fc898622ff3b8fcbefc997a6cc5c3861f08746264", + "noHorizontalOverflow": true + }, + { + "host": "django", + "state": "phone", + "file": "django-phone.png", + "viewport": { + "width": 390, + "height": 844 + }, + "pngDimensions": [780, 1688], + "sha256": "19fecc73ea32e63ff44ef8aaae847c7be6aff8f44f08aaea8fe59a57abc41b00", + "noHorizontalOverflow": true + }, + { + "host": "nethttp", + "state": "desktop", + "file": "nethttp-desktop.png", + "viewport": { + "width": 1440, + "height": 1000 + }, + "pngDimensions": [1280, 889], + "sha256": "0f14268265667bedf32a8a4fc898622ff3b8fcbefc997a6cc5c3861f08746264", + "noHorizontalOverflow": true + }, + { + "host": "nethttp", + "state": "phone", + "file": "nethttp-phone.png", + "viewport": { + "width": 390, + "height": 844 + }, + "pngDimensions": [780, 1688], + "sha256": "19fecc73ea32e63ff44ef8aaae847c7be6aff8f44f08aaea8fe59a57abc41b00", + "noHorizontalOverflow": true + }, + { + "host": "gin", + "state": "desktop", + "file": "gin-desktop.png", + "viewport": { + "width": 1440, + "height": 1000 + }, + "pngDimensions": [1280, 889], + "sha256": "0f14268265667bedf32a8a4fc898622ff3b8fcbefc997a6cc5c3861f08746264", + "noHorizontalOverflow": true + }, + { + "host": "gin", + "state": "phone", + "file": "gin-phone.png", + "viewport": { + "width": 390, + "height": 844 + }, + "pngDimensions": [780, 1688], + "sha256": "19fecc73ea32e63ff44ef8aaae847c7be6aff8f44f08aaea8fe59a57abc41b00", + "noHorizontalOverflow": true + }, + { + "host": "laravel", + "state": "desktop", + "file": "laravel-desktop.png", + "viewport": { + "width": 1440, + "height": 1000 + }, + "pngDimensions": [1280, 889], + "sha256": "0f14268265667bedf32a8a4fc898622ff3b8fcbefc997a6cc5c3861f08746264", + "noHorizontalOverflow": true + }, + { + "host": "laravel", + "state": "phone", + "file": "laravel-phone.png", + "viewport": { + "width": 390, + "height": 844 + }, + "pngDimensions": [780, 1688], + "sha256": "19fecc73ea32e63ff44ef8aaae847c7be6aff8f44f08aaea8fe59a57abc41b00", + "noHorizontalOverflow": true + }, + { + "host": "symfony", + "state": "desktop", + "file": "symfony-desktop.png", + "viewport": { + "width": 1440, + "height": 1000 + }, + "pngDimensions": [1280, 889], + "sha256": "0f14268265667bedf32a8a4fc898622ff3b8fcbefc997a6cc5c3861f08746264", + "noHorizontalOverflow": true + }, + { + "host": "symfony", + "state": "phone", + "file": "symfony-phone.png", + "viewport": { + "width": 390, + "height": 844 + }, + "pngDimensions": [780, 1688], + "sha256": "19fecc73ea32e63ff44ef8aaae847c7be6aff8f44f08aaea8fe59a57abc41b00", + "noHorizontalOverflow": true + }, + { + "host": "yew", + "state": "desktop", + "file": "yew-desktop.png", + "viewport": { + "width": 1440, + "height": 1000 + }, + "pngDimensions": [1280, 889], + "sha256": "f9b08de51239107c4f69416704fa11d1efda39befad417eb1e7e823604d671a8", + "noHorizontalOverflow": true + }, + { + "host": "yew", + "state": "phone", + "file": "yew-phone.png", + "viewport": { + "width": 390, + "height": 844 + }, + "pngDimensions": [780, 1688], + "sha256": "bc0c65bded70cda43b799ce0a69fff9dca6ca25aa8cc1491e77220d3edd9ce53", + "noHorizontalOverflow": true + }, + { + "host": "aspnet", + "state": "phone", + "file": "aspnet-phone.png", + "viewport": { + "width": 390, + "height": 844 + }, + "pngDimensions": [780, 1688], + "sha256": "06b7c0619e46ab13c39235d910b85666e560232debe56eca11445caaf98e26a5", + "noHorizontalOverflow": true + }, + { + "host": "blazor", + "state": "desktop", + "file": "blazor-desktop.png", + "viewport": { + "width": 1440, + "height": 1000 + }, + "pngDimensions": [1280, 889], + "sha256": "98106da5ea56b2433848c8dc53e2a9e30e9162d5796561b387d664b62f663f29", + "noHorizontalOverflow": true + }, + { + "host": "blazor", + "state": "phone", + "file": "blazor-phone.png", + "viewport": { + "width": 390, + "height": 844 + }, + "pngDimensions": [780, 1688], + "sha256": "2b9730aa5885f49f95ca44c7a0296829e2cf6b5561eefe3023a53f37d79ad005", + "noHorizontalOverflow": true + }, + { + "host": "aspnet", + "state": "desktop", + "file": "aspnet-desktop.png", + "viewport": { + "width": 1440, + "height": 640 + }, + "pngDimensions": [1280, 569], + "sha256": "25c50d4a64268e6b63be331e6acd8489149234a54a2cb640ff0c564a39fee271", + "noHorizontalOverflow": true + }, + { + "host": "blazor", + "state": "token-unavailable", + "file": "blazor-token-unavailable.png", + "viewport": { + "width": 1440, + "height": 640 + }, + "pngDimensions": [1280, 569], + "sha256": "de52b8d598093de6f9a4240b3f1f61d8e66eb2290361d0921464f9cc61c5432e", + "noHorizontalOverflow": true + } + ], + "buildResults": [ + { + "label": "python-web_host-assets", + "exit": 0, + "seconds": 0.52, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "label": "go-web-host-assets", + "exit": 0, + "seconds": 0.28, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "label": "php-web-host-assets", + "exit": 0, + "seconds": 0.27, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "label": "dotnet-assets", + "exit": 0, + "seconds": 0.06, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "label": "rust-trunk", + "exit": 2, + "seconds": 0.03, + "sha256": "db3a86165db664e0eed862a259bb4427bf0f710967421eda6efba57c2dd44ff5" + }, + { + "label": "rust-assets", + "exit": 0, + "seconds": 0.04, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "label": "go-host-build", + "exit": 0, + "seconds": 9.54, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "label": "rust-trunk", + "exit": 0, + "seconds": 33.07, + "sha256": "7c106918a658cd9bc3300c081e964dada6691bdf006f8dd3539437495035b18f" + }, + { + "label": "rust-server-build", + "exit": 0, + "seconds": 5.13, + "sha256": "9ddd0cba872bcdfb878adc39f69fc176989ef9d0b1c390f41c3a23b3334369ab" + }, + { + "label": "aspnet-build", + "exit": 0, + "seconds": 8.79, + "sha256": "944b57315ce6eff7c7bb129a3385ea7d54364556ab4f8060c4b6b03cd7326f47" + }, + { + "label": "blazor-publish", + "exit": 0, + "seconds": 8.14, + "sha256": "1c0b575c2f7dffcab3f8cae2ee3ba7b036b88c106eef79e7027e4046f7fc317d" + }, + { + "label": "blazor-prepare", + "exit": 0, + "seconds": 0.09, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "label": "maui-build", + "exit": 0, + "seconds": 13.31, + "sha256": "ae1eca8e9ccf047940338a589aa85cda4f3085da376c089b807dc51fba0b37ae" + } + ] +} diff --git a/docs/images/native-sdk-examples/nethttp-desktop.png b/docs/images/native-sdk-examples/nethttp-desktop.png new file mode 100644 index 00000000..1c13680c Binary files /dev/null and b/docs/images/native-sdk-examples/nethttp-desktop.png differ diff --git a/docs/images/native-sdk-examples/nethttp-phone.png b/docs/images/native-sdk-examples/nethttp-phone.png new file mode 100644 index 00000000..ea496c81 Binary files /dev/null and b/docs/images/native-sdk-examples/nethttp-phone.png differ diff --git a/docs/images/native-sdk-examples/symfony-desktop.png b/docs/images/native-sdk-examples/symfony-desktop.png new file mode 100644 index 00000000..1c13680c Binary files /dev/null and b/docs/images/native-sdk-examples/symfony-desktop.png differ diff --git a/docs/images/native-sdk-examples/symfony-phone.png b/docs/images/native-sdk-examples/symfony-phone.png new file mode 100644 index 00000000..ea496c81 Binary files /dev/null and b/docs/images/native-sdk-examples/symfony-phone.png differ diff --git a/docs/images/native-sdk-examples/yew-desktop.png b/docs/images/native-sdk-examples/yew-desktop.png new file mode 100644 index 00000000..8b809131 Binary files /dev/null and b/docs/images/native-sdk-examples/yew-desktop.png differ diff --git a/docs/images/native-sdk-examples/yew-phone.png b/docs/images/native-sdk-examples/yew-phone.png new file mode 100644 index 00000000..63a71e1e Binary files /dev/null and b/docs/images/native-sdk-examples/yew-phone.png differ diff --git a/docs/mr-c-opaque-credentials.md b/docs/mr-c-opaque-credentials.md new file mode 100644 index 00000000..94100437 --- /dev/null +++ b/docs/mr-c-opaque-credentials.md @@ -0,0 +1,24 @@ +# Opaque credentials and the coordinated API cutover + +Source contract: polymorfa/polymorfa #647, checkpoint e95d02da674da8242453c2a41efd48883fa04734. +This SDK PR updates all six languages to validate server credential prefixes, +canonical base64url and CRC-32/ISO-HDLC. It never authenticates a credential and +contains no signing key. Shape validation permits growth and asserts no exact +total length. The API validates its supported format and live issuance records. + +Treat every credential as an opaque string. Never parse it or assume a fixed +length. Credential storage must support at least 512 characters, preferably +unrestricted text. Client-token lengths vary with their bindings, Customer block +and signing kid; pass them unchanged and keep browser tokens in memory. + +Team keys contain 20 CSPRNG bytes and a four-byte big-endian CRC32 after +`pmfa_`; project tokens use `pmfa_pt_`. The current totals are 37/40 characters, +provided as fixture evidence, not storage or client parsing limits. CRC is not +authentication. Test keys are ordinary project tokens restricted by live server +authority; clients do not infer the restriction from the string. + +Publication and CLI dependency pinning must follow accepted API deployment and +source/package verification. Source merge alone does not establish availability. +The native SDK foundation #348 remains a stack dependency until merged to main. + +The shared fixture is `contracts/machine-credentials.v1.json`; its exact producer, blob and SHA-256 are recorded in `contracts/machine-credentials.source.json`. This separately pinned supplement does not advance the baseline API version. SDK framing acceptance never verifies issuance, signatures, claims or server policy. diff --git a/docs/native-release.md b/docs/native-release.md new file mode 100644 index 00000000..d89f801c --- /dev/null +++ b/docs/native-release.md @@ -0,0 +1,116 @@ +# Native SDK publication setup + +`.github/workflows/native-release.yml` prepares and publishes the native SDKs +from `main` only, by manual dispatch. Nothing in this repository has been +published by it yet. GitHub release environments and immutable Go tag protection +are configured. Registry trust policies and account access remain prerequisites. + +## What the workflow does + +1. **gate** requires the exact deployed API acceptance receipt for the channel + (staging for nightly, production for stable), every feature contract pin, + including frequency caps, segments/drips and client-token ceilings, reconciled with the + release contract, and complete native coverage. It computes one version and + writes `release-contract.json`, binding source SHA, fingerprint, API + acceptance and every registry version form. +2. **pack** verifies each language, stamps the accepted version and receipt into + build inputs (never committed to `main`), builds every public artifact and + inspects it. The PHP job also prepares the Composer mirror tree from the + inspected archive. +3. **plan** downloads every accepted pack, re-inspects it and writes + `publish-plan.json`: 16 publications in dependency order, each with its + artifact SHA-256 values. +4. One job per registry re-checks the plan against the downloaded receipt and + artifacts before any effect. It refuses to overwrite an existing version with + different contents, treats an existing identical version as done, stops + dependents when a dependency is not confirmed, and re-reads the registry + after an upload error instead of retrying. + +| Registry | Packages, in order | Job credential | +| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| crates.io | `polymorfa-storage-core`, then `polymorfa-sdk` and `polymorfa-storage-indexeddb` | Trusted publishing (`rust-lang/crates-io-auth-action`), `id-token: write` | +| NuGet | `Polymorfa.Sdk`, `Polymorfa.Storage.Sql`, then `LibSql`, `EntityFrameworkCore`, `MongoDb`, `DynamoDb`, `Firestore`, `Cosmos`, `S3`, `IndexedDb` | Trusted publishing (`NuGet/login`), `id-token: write` | +| PyPI | `polymorfa-sdk` wheel and sdist | Trusted publishing (`pypa/gh-action-pypi-publish`), `id-token: write` | +| Go | `github.com/polymorfa/sdks/packages/go` tag `packages/go/v` | `GITHUB_TOKEN` with `contents: write` in that job only | +| Packagist | `polymorfa/sdk` from a dedicated mirror repository | GitHub App token for the mirror repository only | + +Internal packages build with one accepted version. Rust requirements are +`=` with no path; NuGet sibling dependencies are emitted as +``, NuGet's inclusive minimum, which the resolver satisfies with that +exact version when it exists. Third-party pins keep their committed ranges. + +Credentials reach registries only through environment variables and request +headers. The crates.io job uploads the exact accepted `.crate` bytes through the +registry publish API instead of `cargo publish`, which would repack them. NuGet +compares packages without the repository signature nuget.org adds. + +### Go module release commit + +Go has no upload registry. `scripts/go-release-tag.mjs` creates a commit whose +parent is the accepted `main` SHA and whose only changes are +`packages/go/transport.go` and `packages/go/calls_diagnostics.go` (the version +stamp) and `packages/go/release-contract.json`. It tags that commit, downloads +the module through the Go command from the local repository and checks that the +module zip contains the stamped version and receipt. It then pushes only the +tag. No branch is created or moved. An existing tag at a different commit is a +hard failure and is never moved; an existing tag at the same commit resumes. +Afterwards the job requires `proxy.golang.org` to serve the same `h1:` hash. + +### PHP mirror + +`scripts/php-mirror-publish.mjs` writes exactly the files of the inspected +archive (the mirror plan) into the mirror repository root, binds the commit +message to the accepted source and archive hashes, and fast-forwards `main`. +Stable releases also push the tag `v`; nightlies are `dev-main`. It +never forces a branch or moves a tag, and it requires Packagist metadata to +reference the pushed commit. + +## Publication prerequisites + +Complete repository configuration through authorized GitHub access. Registry +account actions require access to the owning account; a repository permission +does not grant registry ownership. + +1. **GitHub environments** in `polymorfa/sdks`: `native-sdk-nightly` + and `native-sdk-stable` exist with only `main` allowed. No required reviewer + was added. Manual dispatch and the exact acceptance gate control entry; + registry trust must name the matching environment. +2. **Tag protection**: the active Go ruleset for `refs/tags/packages/go/v*` + blocks updates and deletion without bypass actors. +3. **crates.io** (crate owner): crates.io trusted publishing cannot create a + crate. Publish the first version of each of the three crates manually, in + dependency order, using the accepted artifacts from a reviewed workflow run, + with a short-lived token scoped to `publish-new` and revoked afterwards. Then + add a trusted publisher for each crate: repository `polymorfa/sdks`, workflow + `native-release.yml`, environment `native-sdk-nightly` and, separately, + `native-sdk-stable`. +4. **NuGet** (nuget.org owner of the `Polymorfa.*` prefix): add a trusted + publishing policy for owner `polymorfa`, repository `sdks`, workflow file + `native-release.yml`, scoped to the ten package IDs including new-package + publication. Set repository variable `NUGET_USER` to the nuget.org profile + name. Consider reserving the `Polymorfa.` ID prefix. +5. **PyPI** (project owner): add a pending trusted publisher for project + `polymorfa-sdk`, owner `polymorfa`, repository `sdks`, workflow + `native-release.yml`, environment `native-sdk-nightly` (and a second for + `native-sdk-stable`). A pending publisher creates the project on first + upload; it does not reserve the name. +6. **PHP mirror** (organization owner and Packagist maintainer): the empty + `polymorfa/sdk-php` repository exists, and `PHP_MIRROR_REPOSITORY` is + `sdk-php`. Its `main` history and `v*` tags are protected against rewriting + and deletion. Install a GitHub App with + contents write on that repository only; store its client ID in variable + `PHP_MIRROR_APP_CLIENT_ID` and private key in environment secret + `PHP_MIRROR_APP_PRIVATE_KEY`. Submit the mirror to Packagist as + `polymorfa/sdk` and enable the Packagist GitHub integration so pushes update + it. +7. **API acceptance**: the gate reads the backend repository's + `api-contract-acceptance.yml` artifacts through the existing + `SDK_COVERAGE_CLIENT_ID` / `SDK_COVERAGE_APP_PRIVATE_KEY` app, which must be + available to both environments. + +## Verification without registries + +`node --test scripts/native-publish.test.mjs` exercises planning, stale or +tampered receipts, dependency stops, resumption and overwrite refusal against +recording substitutes. A fixture run proves control flow only; it does not show +that any registry accepts the artifacts, and no package is published by it. diff --git a/docs/native-sdk-release.md b/docs/native-sdk-release.md new file mode 100644 index 00000000..f59cffb4 --- /dev/null +++ b/docs/native-sdk-release.md @@ -0,0 +1,29 @@ +# Native SDK release procedure + +Use the [native SDK publication setup](native-release.md) for the current +workflow, registry identities, credentials, dependency order and release gates. +`native-release.yml` prepares and publishes accepted artifacts from reviewed +`main` by manual dispatch. Preparation and publication are separate jobs; +passing local checks or configuring credentials does not publish a package. + +Nightlies require matching staging API acceptance. Stable packages require +matching production acceptance. Every contract snapshot and feature supplement, +including client-token ceilings, must reconcile with that exact accepted +producer. Keep publication holds until this reconciliation is reviewed. + +The workflow binds inspected package bytes to `release-contract.json`, confirms +registry contents after publication and refuses conflicting existing versions. +Go tags and Composer mirror commits follow the same accepted source. A local +archive, feed, path install or extracted-crate patch proves packaging only. + +## Customer documentation + +After publication, verify each advertised install command in a clean environment +against its registry. Then update the matching Mintlify language guides, +overview, navigation and shipped-day changelog in `polymorfa/polymorfa` with the +actual package versions and coverage. Keep release-dependent guides and install +claims private until those checks pass. + +Signal implementation remains with its owning workstream. Source presence and +SDK methods do not establish deployed custody, history, pairing or browser +tunnel availability. diff --git a/docs/native-server-sdks.md b/docs/native-server-sdks.md new file mode 100644 index 00000000..93490c59 --- /dev/null +++ b/docs/native-server-sdks.md @@ -0,0 +1,111 @@ +# Native server SDK development + +The Go, Python, PHP, Rust and .NET packages are handwritten development +implementations. They are not registry releases. Each package documents its +tested resources and remaining gaps. Do not infer full parity, live API +availability or enrollment from an installed client. + +The implementation reference is the TypeScript server SDK at +`e24cb176fc1124f5fe852ff6c77902648fd13b4a`, recorded as `sourceSdkCommit` in +each native `coverage.json`. It pins the TypeScript implementation the native +ports mirror. It is not API acceptance, deployed availability or registry +publication. The full Messaging and Platform snapshots remain pinned to API +source `171a9683ff45d11a4b5d3917dc428ad2722b0d99`; supplements keep their exact +recorded revisions. No global contract repin is part of this change. + +`contracts/native-campaign-supplements.json` maps the eleven contact +frequency-cap, audience-segment and drip operations, plus the affected field +shapes, from the two integrated campaign supplements to each language's public +method and wire test. These operations are outside the 579-operation ledger; +the inventory does not equate that ledger with any later API operation count. +`scripts/check-native-coverage.mjs` checks each mapping against the pinned +supplement, the package source and the named test. + +| Language | Source | Intended package | Baseline | +| -------- | ----------------- | --------------------------------------- | ----------- | +| Go | `packages/go` | `github.com/polymorfa/sdks/packages/go` | Go 1.24 | +| Python | `packages/python` | `polymorfa-sdk`, import `polymorfa` | Python 3.10 | +| PHP | `packages/php` | `polymorfa/sdk` | PHP 8.2 | +| Rust | `packages/rust` | `polymorfa-sdk` | Rust 1.85 | +| C# | `packages/dotnet` | `Polymorfa.Sdk` | .NET 8 | + +Go's module path follows the actual repository subdirectory. The original +design's `github.com/polymorfa/sdks/go` would resolve source from a different +directory. Go version tags must use `packages/go/v`, as described in +the [Go module source guide](https://go.dev/doc/modules/managing-source). + +Run `node scripts/verify-native-sdk.mjs ` from the repository root. +Native tools must be installed. The native CI matrix verifies each baseline +and a newer runtime and builds package artifacts. This workflow has no registry +publication step or publication credentials. + +## Contract and coverage evidence + +The schema-version-2 ledger declares all six server SDK languages. Each +operation has a mapping for each language. `missing` and `partial` require a +reason and milestone; `covered` requires a public method. The checker keeps +per-language gaps visible even when TypeScript covers the same operation. +Tracker reports include the affected-language matrix. + +Each native package's tests must resolve its public methods and exercise the +request, response, errors and metadata. Shared behavior fixtures and the +loopback mock server are described in +[`contracts/fixtures/README.md`](../contracts/fixtures/README.md). +Raw requests, method manifests and ledger counts do not prove typed parity. +Retain explicit gaps until native tests pass. + +## Ownership and availability + +Signal integration is owned by the Signal thread. Direct media, direct +history and self-hosted storage depend on their merged TypeScript references +and matching backend contracts. This work does not edit those branches or +define a second protocol. + +The SDKs add no feature evaluator, entitlement, enrollment or admin control. +The API remains the authority for scopes, customer membership, release +eligibility and paid capabilities. An SDK method does not grant access. + +## Release preparation + +Prepare PyPI wheels/sdists, crates.io crates, NuGet packages, Composer archives +and Go module source tags through native tools. The publication jobs, the +package graph and the operator prerequisites are described in +[`native-release.md`](native-release.md). Before publication, verify +full claimed coverage, package contents, baseline runtimes and the same API +acceptance receipt used by the npm release gate. Nightlies require staging +acceptance; stable releases require production acceptance. Registry ownership +and release credentials must be configured separately. + +Publication, tags and releases require an explicit release instruction. +Do not add a release changelog or public Mintlify installation guide until the +package exists for the stated audience. Language documentation in this repo +describes source development; the release checklist must publish the matching +Mintlify guides and navigation with the packages. + +## Direct media and history submissions + +The held API source `f97ad78b172e80788c33bdc0fcf28f75db9283ec` has no +route-specific Messaging idempotency ledger for these four POST operations: + +- `/messaging/{session}/media/upload-grants` +- `/messaging/{session}/media/uploads` +- `/messaging/{session}/history/chunks/retry` +- `/messaging/{session}/history/media/delete` + +Each operation submits at most once per invocation in TypeScript, Go, Python, +PHP, .NET and Rust, even with a nonempty caller idempotency key and an explicit +positive retry count. The key, caller headers, credentials, timeout and +cancellation still apply. A connection failure may mean the effect occurred; +inspect authoritative state before deciding whether to invoke the operation +again. A key or identical request bytes do not prove server deduplication. +The explicit history retry asks the phone for another chunk; it does not enable +automatic HTTP retries. Delete only after durable local processing. + +Ordinary established idempotent sends and GET requests keep their retry policy. +Direct WhatsApp CDN resumable uploads, chunk recovery and grant refresh retain +their separate provider protocol. Each grant request itself submits once. + +These operations remain held, source-only functionality. They are absent from +the accepted `9bb1e2e` API source and are not a new accepted producer or release. +The global API baseline remains `171a9683ff45d11a4b5d3917dc428ad2722b0d99`, +version `2026-09-22`. Signal producer/interface acceptance remains unassigned. diff --git a/docs/official-api-round2.md b/docs/official-api-round2.md new file mode 100644 index 00000000..750336e1 --- /dev/null +++ b/docs/official-api-round2.md @@ -0,0 +1,97 @@ +# Official API round-2 betas + +These client additions prepare the six merged backend contracts in +`contracts/official-api-round2.json`. They do not establish a published package, +deployment or enabled audience. Use the matching accepted SDK prerelease after +publication. API contract dates are independent of package timestamps. + +| Item | TypeScript surface | Scope | +| ----- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| OA-03 | `quickLinks.retrieve`, `CloudReadinessFailureCode`, named send errors | Readiness is checked by the backend. `meta_readiness_unknown` stays pending until cancellation or expiry; it never proves readiness. | +| OA-16 | `catalogs.list/retrieve/sync/listProducts`, incoming `order` in webhook/history types | Native Number routes, `business:read`, enrolled beta. Reads use the snapshot; sync explicitly reads Meta. Prices remain provider values. An invalid order cart is omitted as a whole. | +| OA-17 | `cloudMarketing.getStatus`, refreshed Graph `cloudMarketing.status`, two lifecycle events | Native beta requires `sessions:read`. Unknown values never grant eligibility. Terms are accepted in WhatsApp Manager. The deprecated MM Lite Graph field is removed from the current response type. | +| OA-18 | `contacts.block/unblock`, `parentBsuid`, `contact.update.identityChange` | Use public contact IDs for native blocks. Parent BSUIDs are metadata, never recipients or block targets. Previous identities stay separate. Reuse the existing native methods and caller idempotency keys; inspect uncertain outcomes before starting another operation. | +| OA-21 | `officialGroups.setPicture`, `groups.setPicture`, `chats` history | Official-only picture is a multipart JPEG, square, at least 192 x 192 pixels, at most 5 MB. Server validation checks bytes/dimensions. Hybrid operations use `groups` through Linked Devices only. `hybrid_group_linked_devices_unavailable` means not executed; never retry through Official. History remains opt-in and regional. | +| OA-30 | Platform `client.webhooks.create/update`, `constructWebhookDelivery`, `webhooks.verifyDelivery` | Native-format enrolled beta, opt-in/default off. The older `messaging.webhooks` resource does not accept batching. | + +`feature_unavailable` is a normal typed `PolymorfaError` with its HTTP status, +request ID and code retained. Do not fall back to another API or bypass access +checks. Transport errors and unknown provider results must remain visible. + +Catalog IDs are strings of 1-32 digits. Product pages accept `limit` 1-100 +(default 50) and the API's opaque `nextCursor`. Sync may run for 50 seconds; +its SDK timeout defaults to 60 seconds. It is sent once, even with retry overrides. +A new attempt within 120 seconds returns `catalog_sync_cooldown` with Retry-After. +A failed attempt preserves the previous snapshot except definite `not_linked`. + +Batch settings are `{enabled, maxEvents?, windowMs?}`. Limits are 2-100 events +and 100-1000 ms; defaults are 100 and 1000. Omission on create keeps single +delivery. Omission on update preserves stored settings. Disabling remains +available after beta withdrawal. The server verifies saved format and access. + +Before enabling batching, replace a single-only receiver's +`constructWebhookEvent` with `constructWebhookDelivery`. The result is either +the existing event or `{object:"event_batch",version:1,id,events}`. A window may +contain one event. The original parser still accepts only a single event. +Verify the signature over the whole raw request. Deduplicate individual event +IDs and batch IDs in durable storage. Acknowledge only the whole request; +partial acknowledgement is unsupported. Batch retries retain bytes, membership, +order and IDs. Later admitted work may wait behind earlier endpoint deliveries. + +The compile-checked example is `examples/official-api-round2/index.ts`. +Private Console routes in backend #612, #615 and #616 have no SDK surface. +The same beta delta now has native source and synthetic wire fixtures in Go, +Python, PHP, Rust and C#. The baseline coverage ledger remains separately pinned. +`contracts/native-official-round2.json` attributes the added methods and fields; +it does not certify test execution or package availability. Exact combined-source +hosted qualification is still required. PHP and Rust runtime/package checks have +not run on the constrained local host. + +## Native entry points + +These calls assume an existing server client with an organization API key or +project token. Client tokens cannot access the Number catalog/status methods or +Official groups. Keep the client's normal cancellation and disposal rules. + +| Language | Number cache | Number marketing status | Official picture | Verified delivery | +| ---------- | --------------------------------------------------------------------- | --------------------------------------------------- | --------------------------------------- | ------------------------------ | +| TypeScript | `client.catalogs.list/retrieve/listProducts/sync` | `client.cloudMarketing.getStatus` | `client.officialGroups.setPicture` | `constructWebhookDelivery` | +| Go | `client.Catalogs().List/Retrieve/ListProducts/Sync` | `client.CloudMarketing().GetStatus` | `client.OfficialGroups().SetPicture` | `ConstructWebhookDelivery` | +| Python | `client.catalogs.list/retrieve/list_products/sync` | `client.cloud_marketing.get_status` | `client.official_groups.set_picture` | `construct_webhook_delivery` | +| PHP | `$client->catalogs->list/retrieve/listProducts/sync` | `$client->cloudMarketing->getStatus` | `$client->officialGroups->setPicture` | `Webhooks::constructDelivery` | +| Rust | `client.catalogs().list/retrieve/list_products/sync` | `client.cloud_marketing().get_status` | `client.official_groups().set_picture` | `webhooks::construct_delivery` | +| C# | `client.Catalogs.ListAsync/RetrieveAsync/ListProductsAsync/SyncAsync` | `client.CloudGraph.GetMarketingMessagesStatusAsync` | `client.OfficialGroups.SetPictureAsync` | `Webhooks.ConstructDelivery` | + +Each picture method takes in-memory JPEG bytes, bounds the file to 5 MiB and +sends a multipart `file` with `image/jpeg`. The API checks square dimensions of +at least 192 pixels. The SDK owns the multipart boundary and does not replay an +uncertain upload. The Linked Devices `Groups` picture contract stays separate. + +All six Platform webhook resources accept typed `batching` on create/update and +return saved settings on reads. Use the organization or project resource that +owns the endpoint. Changing a format without supplying batching leaves the API +to validate the stored settings. Disabling batching does not require an enabled +beta. Never infer that omission disables an endpoint's saved batching. + +Go's `VerifiedWebhookDelivery` holds either `Event` or `Batch`. Python and PHP +return their event or `WebhookEventBatch`. Rust returns `WebhookDelivery::Single` +or `WebhookDelivery::Batch`. C# returns `SingleWebhookDelivery` or +`WebhookEventBatch`. These parsers authenticate the original request body before +any JSON/member decoding. A malformed or nested member rejects the delivery. +Single-only receiver methods keep their existing behavior. + +Contact changes keep the new identity on the event and the old identity in +`identityChange.previous`. `parentBsuid` is metadata, never a send/block target. +Incoming `order` values in cloud messages and hosted history contain the API's +validated cart only. They do not imply payment or calculate a total. + +QuickLink `errorCode` remains an open nullable string. The named values +`meta_credentials_invalid`, `meta_phone_not_registered`, +`meta_webhook_not_subscribed`, `meta_payment_method_missing`, +`meta_quality_flagged` and `meta_readiness_unknown` belong to that status field. +They are absent from the public error catalog. Future values remain readable. +Public send failures use the separately named `whatsapp_*` codes in the pinned +contract. The shared synthetic fixture is `contracts/fixtures/official-round2.json`. + +The native examples under `examples/official-api-round2/` demonstrate explicit +catalog pagination. They make no registry installation or availability claim. diff --git a/docs/releasing.md b/docs/releasing.md index 104c6047..5fd54cd0 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -79,6 +79,16 @@ and `npm run build:workspaces`. Packing additionally requires a verified in that disposable checkout. Local packing is not registry publication or hosted API acceptance. +The native Rust check builds and verifies `polymorfa-storage-core` before +`polymorfa-sdk`, retaining both archives under `packages/rust/target/package`. +For unpublished source, Cargo 1.85 resolves the SDK's dependency through a +command-line patch to the extracted, verified core archive. That paired-archive +check does not prove an unpatched registry installation: the packaged lock file +contains a local core entry without its eventual registry checksum. Under +separate publication authority, publish and verify the exact core version first, +then repeat SDK packaging and a fresh consumer installation without that patch. +Do not publish the CI archive as proof that this registry gate has passed. + Roll back an unusable package channel to a previously verified version through an authorized release action. Never overwrite a published version or roll back applied API migrations. API, SDK and CLI rollback decisions must use the same diff --git a/docs/superpowers/plans/2026-08-19-platform-automation-resources.md b/docs/superpowers/plans/2026-08-19-platform-automation-resources.md index 2dbfef01..a7af79ad 100644 --- a/docs/superpowers/plans/2026-08-19-platform-automation-resources.md +++ b/docs/superpowers/plans/2026-08-19-platform-automation-resources.md @@ -15,7 +15,7 @@ - Contract source is the Polymorfa source monorepo at `f156af2dda13e62b6b106a542fdedb39524bdb66`, path `apps/api/docs/openapi.management.json`. - Graph-compatible APIs remain excluded. - Runtime client code is handwritten; OpenAPI does not generate runtime source. -- `PlatformClient` accepts only organization server API keys. +- `PlatformClient` accepts only team keys. - Dashboard-only templates and flows are excluded from this server client. - Open request and response objects remain `Readonly>`; do not invent fields absent from the contract. - Unsafe methods retry only when a caller supplies an idempotency key. @@ -238,7 +238,7 @@ expect(mapping("createFlow").typescript.status).toBe("excluded"); ``` - [ ] **Step 2: Run the focused coverage test and verify it fails with the old 32/249/50 totals** -- [ ] **Step 3: Change the 27 API-key operations to exact covered mappings and the ten dashboard-only operations to exclusions with this reason: `This dashboard-only route does not accept the organization server API key used by PlatformClient.`** +- [ ] **Step 3: Change the 27 API-key operations to exact covered mappings and the ten dashboard-only operations to exclusions with this reason: `This dashboard-only route does not accept the team key used by PlatformClient.`** - [ ] **Step 4: Extend root export type assertions for `PlatformPayload` and each resource class** - [ ] **Step 5: Run `npm run check:coverage`, focused coverage/export tests, and `npm run check:names`** - [ ] **Step 6: Commit with `chore: record Platform automation coverage`** diff --git a/docs/superpowers/specs/2026-08-19-platform-automation-resources-design.md b/docs/superpowers/specs/2026-08-19-platform-automation-resources-design.md index ace61104..13894260 100644 --- a/docs/superpowers/specs/2026-08-19-platform-automation-resources-design.md +++ b/docs/superpowers/specs/2026-08-19-platform-automation-resources-design.md @@ -8,7 +8,7 @@ ## Goal Expand the handwritten TypeScript server SDK with the customer-facing -Platform operations that accept an organization API key and are needed for +Platform operations that accept an team key and are needed for campaign automation. Preserve the credential boundary by excluding endpoints that require a live dashboard session. @@ -40,7 +40,7 @@ key. It must not accept a dashboard JWT, project token, or browser client token. The ten `/v1/templates*` and `/v1/flows*` operations are excluded from the server SDK coverage target because the pinned contract marks them -`x-dashboard-only` and explicitly rejects organization keys and project tokens. +`x-dashboard-only` and explicitly rejects team keys and project tokens. They belong in the later application-server adapter milestone, where an application can authorize a dashboard user without exposing a privileged token to browser code. diff --git a/eslint.config.js b/eslint.config.js index 5b0f914f..6a60b5e0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,12 @@ import tseslint from "typescript-eslint"; export default tseslint.config( { - ignores: ["node_modules/**", "packages/*/dist/**", "coverage/**"], + ignores: [ + "node_modules/**", + "packages/*/dist/**", + "coverage/**", + "packages/typescript/src/history/gen/**", + ], }, eslint.configs.recommended, ...tseslint.configs.recommended, diff --git a/examples/calls-example/client-rules.test.mjs b/examples/calls-example/client-rules.test.mjs index 0058f44a..119444e8 100644 --- a/examples/calls-example/client-rules.test.mjs +++ b/examples/calls-example/client-rules.test.mjs @@ -34,7 +34,7 @@ function fakeMessaging({ }, mint: async (input) => { calls.push(["mint", input]); - return { data: { data: { token: "pmfa_ct_example" } } }; + return { data: { data: { token: "pmfa_ct_ZXhhbXBsZQ" } } }; }, updateRules: async (_session, input) => { calls.push(["update-rules", input]); @@ -79,7 +79,7 @@ test("mint helper rejects missing Calls actions before minting", async () => { test("mint helper uses existing rules without requiring sessions:read", async () => { const messaging = fakeMessaging({ sessionError: forbidden }); const token = await mintCallsExampleToken(messaging, "support", "local-1"); - assert.equal(token, "pmfa_ct_example"); + assert.equal(token, "pmfa_ct_ZXhhbXBsZQ"); assert.deepEqual(messaging.calls, [ "retrieve-rules", ["mint", { session: "support", ephemeralId: "local-1", ttlSeconds: 900 }], diff --git a/examples/calls-example/configure-client-rules.mjs b/examples/calls-example/configure-client-rules.mjs index 330be13f..dc48fc44 100644 --- a/examples/calls-example/configure-client-rules.mjs +++ b/examples/calls-example/configure-client-rules.mjs @@ -1,3 +1,4 @@ +import { serverCredentialType } from "./server-credential.mjs"; import { MessagingClient } from "@polymorfa/sdk"; import { CallsExampleSetupError } from "./mint-client-token-core.mjs"; import { configureCallsExampleRules } from "./configure-client-rules-core.mjs"; @@ -5,11 +6,7 @@ import { configureCallsExampleRules } from "./configure-client-rules-core.mjs"; const key = process.env.POLYMORFA_SERVER_KEY; const session = process.env.POLYMORFA_SESSION; const testCallee = process.env.POLYMORFA_TEST_CALLEE; -const credentialType = key?.startsWith("pmfa_pt_") - ? "projectToken" - : /^pmfa_[A-Za-z0-9_-]{72}$/.test(key ?? "") - ? "apiKey" - : null; +const credentialType = serverCredentialType(key); if (!key || !session || !testCallee) { process.stderr.write( @@ -17,9 +14,7 @@ if (!key || !session || !testCallee) { ); process.exitCode = 1; } else if (!credentialType) { - process.stderr.write( - "Enter a canonical organization key or project token.\n", - ); + process.stderr.write("Enter a canonical team key or project token.\n"); process.exitCode = 1; } else { try { diff --git a/examples/calls-example/mint-client-token.mjs b/examples/calls-example/mint-client-token.mjs index 9cfb6167..6c8b6aa6 100644 --- a/examples/calls-example/mint-client-token.mjs +++ b/examples/calls-example/mint-client-token.mjs @@ -1,3 +1,4 @@ +import { serverCredentialType } from "./server-credential.mjs"; import { MessagingClient } from "@polymorfa/sdk"; import { CallsExampleSetupError, @@ -7,20 +8,14 @@ import { const key = process.env.POLYMORFA_SERVER_KEY; const session = process.env.POLYMORFA_SESSION; const ephemeralId = process.env.POLYMORFA_EPHEMERAL_ID; -const credentialType = key?.startsWith("pmfa_pt_") - ? "projectToken" - : /^pmfa_[A-Za-z0-9_-]{72}$/.test(key ?? "") - ? "apiKey" - : null; +const credentialType = serverCredentialType(key); if (!key || !session || !ephemeralId) { process.stderr.write( "Set POLYMORFA_SERVER_KEY, POLYMORFA_SESSION and POLYMORFA_EPHEMERAL_ID.\n", ); process.exitCode = 1; } else if (!credentialType) { - process.stderr.write( - "Enter a canonical organization key or project token.\n", - ); + process.stderr.write("Enter a canonical team key or project token.\n"); process.exitCode = 1; } else { try { diff --git a/examples/calls-example/server-credential.mjs b/examples/calls-example/server-credential.mjs new file mode 100644 index 00000000..89558703 --- /dev/null +++ b/examples/calls-example/server-credential.mjs @@ -0,0 +1,12 @@ +import { validateClientCredential } from "@polymorfa/sdk"; + +export function serverCredentialType(value) { + if (typeof value !== "string") return null; + const type = value.startsWith("pmfa_pt_") ? "projectToken" : "apiKey"; + try { + validateClientCredential({ type, value }); + return type; + } catch { + return null; + } +} diff --git a/examples/calls-example/server-credential.test.mjs b/examples/calls-example/server-credential.test.mjs new file mode 100644 index 00000000..4c8632b0 --- /dev/null +++ b/examples/calls-example/server-credential.test.mjs @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { serverCredentialType } from "./server-credential.mjs"; + +test("Calls setup accepts v2 server credentials and rejects v1 and other principals", () => { + assert.equal( + serverCredentialType("pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k"), + "apiKey", + ); + assert.equal( + serverCredentialType("pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k"), + "projectToken", + ); + for (const value of [ + undefined, + "pmfa_" + "A".repeat(72), + "pmfa_pt_" + "A".repeat(94), + "pmfa_pt_bad", + "pmfa_ct_Y2xpZW50", + "pmfa_ls_listener", + ]) { + assert.equal(serverCredentialType(value), null); + } +}); diff --git a/examples/full-platform/README.md b/examples/full-platform/README.md index 17cab10b..f267a5ee 100644 --- a/examples/full-platform/README.md +++ b/examples/full-platform/README.md @@ -232,11 +232,11 @@ Polymorfa. | -------------------------------------------------- | --------------------------------------------------------- | | `app/api/desk/*` | Project token through `MessagingClient` (or demo data) | | `app/api/messaging/*` | Project token through `MessagingClient` | -| `app/api/messaging/media` upload action | Organization API key (`Client.media`), admin role only | -| `app/api/admin/*` except the rows below | Organization API key through `Client` | +| `app/api/messaging/media` upload action | Team key (`Client.media`), admin role only | +| `app/api/admin/*` except the rows below | Team key through `Client` | | `app/api/admin/events`, `webhooks` | Project token through `Client<"project">` | -| `app/api/admin/webhooks` `GET ?owner=organization` | Organization API key through `Client` | -| `app/api/admin/settings` | Organization API key and project token views | +| `app/api/admin/webhooks` `GET ?owner=organization` | Team key through `Client` | +| `app/api/admin/settings` | Team key and project token views | | `app/api/admin/bridge` | Project token (`BridgeClient`), no key for `SystemClient` | | Browser pages | Short-lived `pmfa_ct_` client tokens minted by the server | diff --git a/examples/full-platform/app/api/admin/campaigns/route.ts b/examples/full-platform/app/api/admin/campaigns/route.ts index eb4a7af0..7d982919 100644 --- a/examples/full-platform/app/api/admin/campaigns/route.ts +++ b/examples/full-platform/app/api/admin/campaigns/route.ts @@ -3,6 +3,8 @@ import { env } from "../../../../lib/env.js"; import { action, idempotencyKey, + integer, + flag, route, text, unknownAction, @@ -38,9 +40,21 @@ export const POST = route("admin", async ({ body, request }) => { case "delete": return campaigns.delete(id, scope); case "launch": - return campaigns.launch(id, payload, { - idempotencyKey: idempotencyKey(request), - }); + return campaigns.launch( + id, + { + ...scope, + ...(payload.scheduledAt === undefined + ? {} + : { scheduledAt: integer(payload, "scheduledAt") }), + ...(payload.skipMissingVariables === undefined + ? {} + : { skipMissingVariables: flag(payload, "skipMissingVariables") }), + }, + { + idempotencyKey: idempotencyKey(request), + }, + ); case "pause": return campaigns.pause(id); case "resume": diff --git a/examples/full-platform/app/api/messaging/media/route.ts b/examples/full-platform/app/api/messaging/media/route.ts index 4540b12e..0c515d95 100644 --- a/examples/full-platform/app/api/messaging/media/route.ts +++ b/examples/full-platform/app/api/messaging/media/route.ts @@ -67,7 +67,7 @@ export const POST = route("agent", async ({ body, operator, request }) => { return messaging().media.persist(text(body, "mediaId")); case "upload": { // Upload URLs come from the management API, which accepts only an - // organization key, so this action is limited to admins. The payload is + // team key, so this action is limited to admins. The payload is // built here from validated fields; callers never choose the project. // Send the uploaded file afterwards with the messages "send" action. if (operator.role !== "admin") { diff --git a/examples/full-platform/lib/desk/live.ts b/examples/full-platform/lib/desk/live.ts index 5590a212..2030c8ef 100644 --- a/examples/full-platform/lib/desk/live.ts +++ b/examples/full-platform/lib/desk/live.ts @@ -398,7 +398,7 @@ export class PolymorfaDesk implements DeskData { ); } - /** BanSafe Health needs the organization key; without it health is unknown. */ + /** BanSafe Health needs the team key; without it health is unknown. */ async #health(): Promise> { const scores = new Map(); if (optionalEnv("POLYMORFA_ORGANIZATION_API_KEY") === undefined) diff --git a/examples/go/calls-agent/go.mod b/examples/go/calls-agent/go.mod new file mode 100644 index 00000000..96984eb1 --- /dev/null +++ b/examples/go/calls-agent/go.mod @@ -0,0 +1,9 @@ +module polymorfa-go-calls-agent + +go 1.24.0 + +require github.com/polymorfa/sdks/packages/go v0.0.0 + +require github.com/coder/websocket v1.8.15 // indirect + +replace github.com/polymorfa/sdks/packages/go => ../../../packages/go diff --git a/examples/go/calls-agent/go.sum b/examples/go/calls-agent/go.sum new file mode 100644 index 00000000..4e0a48d6 --- /dev/null +++ b/examples/go/calls-agent/go.sum @@ -0,0 +1,2 @@ +github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= +github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= diff --git a/examples/go/calls-agent/main.go b/examples/go/calls-agent/main.go new file mode 100644 index 00000000..1b12632b --- /dev/null +++ b/examples/go/calls-agent/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "fmt" + polymorfa "github.com/polymorfa/sdks/packages/go" + "log" + "os" + "os/signal" + "syscall" + "time" +) + +func main() { + client, err := polymorfa.NewCallsClient(polymorfa.CallsClientConfig{Config: polymorfa.Config{Credential: polymorfa.Credential{Kind: polymorfa.OrganizationAPIKey, Value: os.Getenv("POLYMORFA_API_KEY")}}, Session: os.Getenv("POLYMORFA_SESSION"), Participant: "go-example"}) + if err != nil { + log.Fatal("Set a valid API key and session: ", err) + } + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + session, err := client.Follow(ctx, polymorfa.CallsFollowOptions{}) + if err != nil { + log.Fatal(err) + } + defer func() { + cleanup, done := context.WithTimeout(context.Background(), 5*time.Second) + defer done() + session.Close(cleanup) + }() + for { + select { + case <-session.Done(): + return + case event := <-session.Events(): + if event.Type == "incoming" && event.Call != nil { + go receive(ctx, event.Call) + } + if event.Error != nil { + fmt.Printf("Calls event: %s\n", event.Type) + } + } + } +} +func receive(ctx context.Context, call *polymorfa.Call) { + exclusive := true + if err := call.Answer(ctx, polymorfa.AcceptCallRequest{Exclusive: &exclusive}); err != nil { + fmt.Println("Call could not be answered.") + return + } + for { + select { + case <-call.Done(): + return + case event := <-call.Events(): + if event.Type == "audio" { + fmt.Printf("Received %d PCM samples\n", len(event.Audio)) + } + } + } +} diff --git a/examples/go/d1-bridge/README.md b/examples/go/d1-bridge/README.md new file mode 100644 index 00000000..efa81983 --- /dev/null +++ b/examples/go/d1-bridge/README.md @@ -0,0 +1,7 @@ +# Customer-owned D1 bridge + +`worker.ts` hosts the shared `@polymorfa/storage/d1-bridge` implementation for native applications. Bind your D1 database as `DB`, set a fixed `STORAGE_AUTHORITY` and `STORAGE_TABLE_PREFIX`, and provision a separate `STORAGE_APPLICATION_SECRET` of 32–256 visible ASCII characters. Keep this secret on your application server and Worker; it is distinct from Polymorfa credentials. Use HTTPS and do not expose it to browsers. + +Create schema-2 tables with the matching prefix during controlled setup. The example keeps remote schema creation disabled. If an operator enables `allowSetup` for initial creation, disable it after setup. This example deploys no resources and publishes no package. + +All clients verify the expected authority, prefix, schema and signed nonce handshake before accepting the bridge's atomic commit capability. Atomicity comes from the Worker's D1 binding transaction, not the D1 administrative REST API. Merge rules, leases, deduplication and checkpoint planning stay in each native SDK. See [the bridge protocol](../../../packages/storage/D1-BRIDGE.md). diff --git a/examples/go/d1-bridge/worker.ts b/examples/go/d1-bridge/worker.ts new file mode 100644 index 00000000..0580198d --- /dev/null +++ b/examples/go/d1-bridge/worker.ts @@ -0,0 +1,23 @@ +import { createD1StorageBridge } from "@polymorfa/storage/d1-bridge"; +import type { D1DatabaseLike } from "@polymorfa/storage/d1"; + +interface Environment { + readonly DB: D1DatabaseLike; + readonly STORAGE_AUTHORITY: string; + readonly STORAGE_TABLE_PREFIX: string; + readonly STORAGE_APPLICATION_SECRET: string; +} + +// Bind these values in the customer's Worker deployment. Never derive an +// authority, prefix or secret from a browser, query string or API credential. +export default { + fetch(request: Request, env: Environment): Promise { + return createD1StorageBridge({ + database: env.DB, + authority: env.STORAGE_AUTHORITY, + tablePrefix: env.STORAGE_TABLE_PREFIX, + secret: env.STORAGE_APPLICATION_SECRET, + allowSetup: false, + }).fetch(request); + }, +}; diff --git a/examples/go/send-message/go.mod b/examples/go/send-message/go.mod new file mode 100644 index 00000000..20a4e341 --- /dev/null +++ b/examples/go/send-message/go.mod @@ -0,0 +1,9 @@ +module polymorfa-go-example + +go 1.24.0 + +require github.com/polymorfa/sdks/packages/go v0.0.0 + +require github.com/coder/websocket v1.8.15 // indirect + +replace github.com/polymorfa/sdks/packages/go => ../../../packages/go diff --git a/examples/go/send-message/go.sum b/examples/go/send-message/go.sum new file mode 100644 index 00000000..4e0a48d6 --- /dev/null +++ b/examples/go/send-message/go.sum @@ -0,0 +1,2 @@ +github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= +github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= diff --git a/examples/go/send-message/main.go b/examples/go/send-message/main.go new file mode 100644 index 00000000..b89726e3 --- /dev/null +++ b/examples/go/send-message/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "time" + + polymorfa "github.com/polymorfa/sdks/packages/go" +) + +func main() { + client, err := polymorfa.NewMessagingClient(polymorfa.Config{Credential: polymorfa.Credential{Kind: polymorfa.OrganizationAPIKey, Value: os.Getenv("POLYMORFA_API_KEY")}}) + if err != nil { + fail(err) + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + session, phone := os.Getenv("POLYMORFA_SESSION"), os.Getenv("POLYMORFA_PHONE") + if session == "" || phone == "" { + fmt.Fprintln(os.Stderr, "Set POLYMORFA_SESSION and POLYMORFA_PHONE before sending.") + os.Exit(1) + } + text := "Hello from Go" + result, err := client.Messages().Send(ctx, session, polymorfa.SendMessageRequest{Conversation: polymorfa.ConversationReference{PhoneNumber: phone}, Content: polymorfa.MessageContent{Text: &text}}) + if err != nil { + fail(err) + } + fmt.Printf("Message accepted: %s (request %s)\n", result.Data.Data.ID, result.Metadata.RequestID) +} +func fail(err error) { + var api *polymorfa.Error + if errors.As(err, &api) { + fmt.Fprintf(os.Stderr, "%s: %s (request %s)\n", api.Kind, api.Message, api.RequestID) + } else { + fmt.Fprintln(os.Stderr, "The request failed.") + } + os.Exit(1) +} diff --git a/examples/go/web-host/.gitignore b/examples/go/web-host/.gitignore new file mode 100644 index 00000000..b17b1868 --- /dev/null +++ b/examples/go/web-host/.gitignore @@ -0,0 +1,4 @@ +/assets/elements.js +/assets/htmx.js +/web-host +/polymorfa-go-web-host diff --git a/examples/go/web-host/README.md b/examples/go/web-host/README.md new file mode 100644 index 00000000..3e3b4caa --- /dev/null +++ b/examples/go/web-host/README.md @@ -0,0 +1,40 @@ +# Go web host + +Run the same application with `net/http` or Gin. templ renders the page and HTMX submits the sign-in and sign-out forms. The browser mounts the existing Polymorfa Web Components; the Go SDK remains on the server. + +From the repository root: + +```sh +npm ci +node examples/go/web-host/build-assets.mjs +cd examples/go/web-host +go mod download +go test -race ./... +go run . -framework gin -port 8080 +``` + +Before the last command, set `EXAMPLE_APP_USERNAME`, `EXAMPLE_APP_PASSWORD`, `POLYMORFA_PROJECT_TOKEN`, `POLYMORFA_SESSION`, `POLYMORFA_RECIPIENT` and `POLYMORFA_WEBHOOK_SECRET` in the server environment. The recipient is the authorized peer's phone number. Open `http://127.0.0.1:8080`. Use `-framework nethttp` for the standard library host. `POLYMORFA_API_BASE_URL` optionally selects an API origin; authenticated plaintext is restricted to loopback. Asset building bundles the workspace Web Components and downloads HTMX 2.0.4 after checking its SHA-256 digest. + +This loopback example has one configured application account. Sign-in creates a random, server-held session with a 30-minute lifetime and an HttpOnly, SameSite=Strict cookie. Application POST routes enforce the application origin. Browser input cannot select an identity, number or recipient. Sign-out removes the server session and stops browser polling. + +The inbox uses a custom data source. `POST /api/polymorfa/send` calls the native Go SDK's `Messages.Send`. The browser supplies text and a UUID `Idempotency-Key`; the server supplies the Number and recipient. Retry an unknown outcome with the same key and text. The example rejects changed text under an existing key and stops accepting new keys after 128 attempts. + +Register `/webhooks/polymorfa` with the API for the assigned Number, native format, `message.received` and `message.ack`, using the same signing secret. The receiver verifies the exact bounded body with the Go SDK's `WebhookHandler`, decodes typed payloads, ignores other Numbers and recipients, and deduplicates event IDs. After a send, it matches the canonical conversation ID returned by the API, since events can omit the phone number. Production webhook delivery requires a reachable endpoint. For local acceptance, expose only this receiver through a temporary tunnel; keep application login and UI on loopback. + +`GET /api/polymorfa/conversation` returns the authenticated user's application-owned conversation. It holds at most 100 messages and 1,024 recent event IDs in memory until this server stops. A browser reload preserves that state; a server restart clears it. This example does not enable HMS. Event streams omit bodies without authorized retention, so this example receives live signed webhooks instead. The browser uses shared Web Components for rendering and polls this native host; it does not send through the TypeScript browser API client. + +Set `POLYMORFA_EXAMPLE_TRANSPORT=local-test-number` when using isolated local test numbers. The page then labels simulated WhatsApp transport. This is a local transport label, not evidence of real WhatsApp-network delivery. + +Before hosting it publicly, integrate the application's real authentication, live membership and Number/recipient authorization, HTTPS origin, durable sessions and transactional message/idempotency storage. Keep project and organization credentials exclusively in the server process. + +`main_test.go` and `conversation_test.go` use real HTTP servers with a fixture upstream to verify both hosts, native credential/version headers, authentication, assigned targets, origin rejection, caller-key replay and conflict handling, webhook signatures, event deduplication, target confinement and conversation reads after reload. Those tests do not prove a deployed or real WhatsApp-network exchange. + +To change the template, regenerate the tracked Go view: + +```sh +go run github.com/a-h/templ/cmd/templ@v0.3.960 generate +``` + +The SDK itself has no Gin, templ or HTMX dependency. This example uses a separate Go module and a local `replace` directive until an SDK release is available. + +The separate authenticated `/api/polymorfa/token` and `/api/polymorfa/connect` routes remain examples of native client-token minting and supplementary linked-device invitations. They are not used by this native-server inbox. Token minting requires the issuer's complete delegation scopes. The API still checks Number assignment, eligibility, consent and funding; these routes do not establish initial pairing or change custody. diff --git a/examples/go/web-host/bridge.js b/examples/go/web-host/bridge.js new file mode 100644 index 00000000..6600620b --- /dev/null +++ b/examples/go/web-host/bridge.js @@ -0,0 +1,150 @@ +import { definePolymorfaElements } from "@polymorfa/elements"; +definePolymorfaElements(); +let disposeInbox; +// Only receipts the host observed from message.ack become chat receipts. The +// send response status is upstream acceptance, never delivery or read. +const observedReceipts = new Map([ + ["delivered", "delivered"], + ["delivery", "delivered"], + ["read", "read"], + ["played", "played"], +]); +function chatMessage({ receipt, ...message }) { + const state = + message.direction === "outbound" + ? observedReceipts.get(receipt) + : undefined; + return state ? { ...message, receipt: { state } } : message; +} +// The browser renders shared components. Every send and incoming message is +// handled by this application's native Go SDK, not the browser API transport. +function nativeInbox(inbox) { + const changes = new Set(); + const messages = new Set(); + let previous = ""; + let stopped = false; + let timer; + const request = async (path, options = {}) => { + const response = await fetch(`/api/polymorfa/${path}`, { + credentials: "same-origin", + ...options, + }); + if (!response.ok) throw new Error(await response.text()); + return response.json(); + }; + const row = (snapshot) => { + const last = snapshot.messages.at(-1); + return { + id: snapshot.peer, + session: snapshot.session, + name: "Assigned recipient", + phoneNumber: snapshot.peer, + lastActivity: last?.createdAt ?? 0, + unreadCount: 0, + ...(last + ? { + lastMessage: { + text: last.text, + createdAt: last.createdAt, + direction: last.direction, + }, + } + : {}), + }; + }; + const poll = async () => { + try { + const snapshot = await request("conversation"); + if (stopped) return; + const serialized = JSON.stringify(snapshot); + if (serialized !== previous) { + previous = serialized; + for (const notify of changes) + notify({ type: "conversation", conversation: row(snapshot) }); + for (const message of snapshot.messages) + for (const notify of messages) + notify({ type: "upsert", message: chatMessage(message) }); + } + const status = document.querySelector("#native-status"); + if (status) { + const outgoing = snapshot.messages.findLast( + (m) => m.direction === "outbound", + ); + status.textContent = + `Go SDK · ${snapshot.receivedEvents} verified webhook events` + + (outgoing ? ` · Latest send: ${outgoing.receipt}` : ""); + } + } catch (error) { + const status = document.querySelector("#native-status"); + if (status && !stopped) status.textContent = error.message; + } finally { + if (!stopped) timer = setTimeout(poll, 800); + } + }; + inbox.source = { + async listConversations(_cursor, signal) { + return { + conversations: [row(await request("conversation", { signal }))], + }; + }, + subscribe(notify) { + changes.add(notify); + return () => changes.delete(notify); + }, + conversation() { + return { + async load(_cursor, signal) { + return { + messages: (await request("conversation", { signal })).messages.map( + chatMessage, + ), + }; + }, + subscribe(notify) { + messages.add(notify); + return () => messages.delete(notify); + }, + async send(message, signal) { + if (message.replyTo || message.attachments?.length) + throw new Error("This native example sends text only."); + return chatMessage( + await request("send", { + method: "POST", + signal, + headers: { + "Content-Type": "application/json", + "Idempotency-Key": message.clientId, + }, + body: JSON.stringify({ text: message.text }), + }), + ); + }, + }; + }, + }; + void poll(); + return () => { + stopped = true; + clearTimeout(timer); + changes.clear(); + messages.clear(); + }; +} +function mount() { + const inbox = document.querySelector("pmfa-inbox"); + if (inbox) { + disposeInbox ??= nativeInbox(inbox); + } else { + disposeInbox?.(); + disposeInbox = undefined; + } +} +document.addEventListener("htmx:afterSwap", mount); +document.addEventListener("htmx:responseError", () => { + const error = document.querySelector("#error"); + if (error) + error.textContent = "The request failed. Check your sign-in and try again."; +}); +if (document.readyState === "loading") + document.addEventListener("DOMContentLoaded", mount, { once: true }); +else mount(); diff --git a/examples/go/web-host/build-assets.mjs b/examples/go/web-host/build-assets.mjs new file mode 100644 index 00000000..a6a2c4af --- /dev/null +++ b/examples/go/web-host/build-assets.mjs @@ -0,0 +1,24 @@ +import { build } from "esbuild"; +import { createHash } from "node:crypto"; +import { writeFile, mkdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +await mkdir(new URL("assets/", import.meta.url), { recursive: true }); +await build({ + entryPoints: [fileURLToPath(new URL("bridge.js", import.meta.url))], + outfile: fileURLToPath(new URL("assets/elements.js", import.meta.url)), + bundle: true, + format: "esm", + platform: "browser", + target: "es2022", +}); +const response = await fetch( + "https://raw.githubusercontent.com/bigskysoftware/htmx/v2.0.4/dist/htmx.min.js", +); +if (!response.ok) throw new Error("Cannot fetch pinned HTMX asset."); +const body = Buffer.from(await response.arrayBuffer()); +if ( + createHash("sha256").update(body).digest("hex") !== + "e209dda5c8235479f3166defc7750e1dbcd5a5c1808b7792fc2e6733768fb447" +) + throw new Error("Pinned HTMX integrity check failed."); +await writeFile(new URL("assets/htmx.js", import.meta.url), body); diff --git a/examples/go/web-host/conversation.go b/examples/go/web-host/conversation.go new file mode 100644 index 00000000..028823aa --- /dev/null +++ b/examples/go/web-host/conversation.go @@ -0,0 +1,206 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + "regexp" + "strings" + "sync" + "time" + + polymorfa "github.com/polymorfa/sdks/packages/go" +) + +// Application-owned example state, never HMS. It lasts for this process only. +// At capacity, sends fail closed rather than forgetting their replay keys. +type conversationState struct { + peerID string + mu sync.Mutex + sendMu sync.Mutex + messages []conversationMessage + attempts map[string]*sendAttempt + eventIDs []string + received int + lastEvent string +} +type conversationMessage struct { + ID string `json:"id"` + ClientID string `json:"clientId,omitempty"` + Text string `json:"text"` + CreatedAt int64 `json:"createdAt"` + Direction string `json:"direction"` + Status string `json:"status"` + Receipt string `json:"receipt,omitempty"` +} +type sendAttempt struct { + text string + message *conversationMessage +} + +var callerKey = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) + +func (a *app) sendMessage(w http.ResponseWriter, r *http.Request) { + if !a.originAllowed(w, r) { + return + } + if _, ok := a.authenticated(r); !ok { + http.Error(w, "Sign in required.", 401) + return + } + key := r.Header.Get("Idempotency-Key") + var input struct { + Text string `json:"text"` + } + decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16384)) + decoder.DisallowUnknownFields() + if decoder.Decode(&input) != nil || decoder.Decode(new(any)) != io.EOF || !callerKey.MatchString(key) || strings.TrimSpace(input.Text) == "" || len(input.Text) > 4096 { + http.Error(w, "Send text and a UUID Idempotency-Key; targets are assigned by the server.", 400) + return + } + if a.peer == "" { + http.Error(w, "No authorized recipient configured.", 503) + return + } + s := &a.conversation + s.sendMu.Lock() + defer s.sendMu.Unlock() + if s.attempts == nil { + s.attempts = map[string]*sendAttempt{} + } + attempt := s.attempts[key] + if attempt != nil { + if attempt.text != input.Text { + http.Error(w, "This key belongs to different text.", 409) + return + } + if attempt.message != nil { + writeJSON(w, attempt.message) + return + } + } else { + if len(s.attempts) >= 128 { + http.Error(w, "Example send capacity reached. Restart the example for a new session.", 429) + return + } + attempt = &sendAttempt{text: input.Text} + s.attempts[key] = attempt + } + result, err := a.client.Messages().Send(r.Context(), a.session, polymorfa.SendMessageRequest{ + Conversation: polymorfa.ConversationReference{PhoneNumber: a.peer}, Content: polymorfa.MessageContent{Text: &input.Text}, + }, polymorfa.RequestOptions{IdempotencyKey: key, Timeout: 60 * time.Second}) + if err != nil || !result.Data.Success || result.Data.Data.ID == "" { + // An interrupted send may have succeeded. Keep its key and text so retry + // asks the API for the same operation, never creates a replacement send. + http.Error(w, "Send outcome unknown. Retry this message with the same key.", 502) + return + } + receipt := result.Data.Data + at, err := time.Parse(time.RFC3339Nano, receipt.Timestamp) + if err != nil { + at = time.Now() + } + message := conversationMessage{ID: receipt.ID, ClientID: key, Text: input.Text, CreatedAt: at.UnixMilli(), Direction: "outbound", Status: "sent", Receipt: receipt.Status} + s.mu.Lock() + if receipt.Conversation.ID != "" { + s.peerID = receipt.Conversation.ID + } + s.upsert(message) + s.mu.Unlock() + attempt.message = &message + writeJSON(w, message) +} + +func (s *conversationState) upsert(message conversationMessage) { + for i, old := range s.messages { + if old.ID == message.ID { + s.messages[i] = message + return + } + } + s.messages = append(s.messages, message) + if len(s.messages) > 100 { + s.messages = append([]conversationMessage(nil), s.messages[len(s.messages)-100:]...) + } +} + +func (a *app) receiveEvent(_ *http.Request, event polymorfa.WebhookEvent) error { + if event.Session != a.session { + return nil + } + payload, known, err := event.TypedPayload() + if err != nil { + return err + } + if !known { + return nil + } + s := &a.conversation + s.mu.Lock() + defer s.mu.Unlock() + for _, id := range s.eventIDs { + if id == event.ID { + return nil + } + } + switch p := payload.(type) { + case *polymorfa.LinkedDeviceMessagePayload: + if event.Event != "message.received" || p.FromMe || p.IsGroup || !a.assignedConversation(p.Conversation) || p.Text == "" || p.ID == "" || len(p.Text) > 4096 { + return nil + } + s.upsert(conversationMessage{ID: p.ID, Text: p.Text, CreatedAt: p.Timestamp * 1000, Direction: "inbound", Status: "sent", Receipt: "received"}) + case *polymorfa.MessageAckPayload: + if !a.assignedConversation(p.Conversation) { + return nil + } + for _, reference := range p.Messages { + for i := range s.messages { + m := &s.messages[i] + if m.ID == reference.ID && m.Direction == "outbound" && receiptRank(p.Type) > receiptRank(m.Receipt) { + m.Receipt = p.Type + } + } + } + default: + return nil + } + s.received++ + s.lastEvent = event.Event + s.eventIDs = append(s.eventIDs, event.ID) + if len(s.eventIDs) > 1024 { + s.eventIDs = s.eventIDs[len(s.eventIDs)-1024:] + } + return nil +} + +// Called under the conversation lock. An established canonical ID takes +// precedence; phoneNumber is optional in events, including LID conversations. +func (a *app) assignedConversation(conversation polymorfa.WebhookConversation) bool { + if a.conversation.peerID != "" { + return conversation.ID == a.conversation.peerID + } + return a.peer != "" && strings.TrimPrefix(conversation.PhoneNumber, "+") == strings.TrimPrefix(a.peer, "+") +} +func receiptRank(value string) int { + switch value { + case "read", "played": + return 3 + case "delivered", "delivery": + return 2 + case "sent", "server": + return 1 + } + return 0 +} + +func (a *app) conversationSnapshot(w http.ResponseWriter, r *http.Request) { + if _, ok := a.authenticated(r); !ok { + http.Error(w, "Sign in required.", 401) + return + } + s := &a.conversation + s.mu.Lock() + defer s.mu.Unlock() + messages := append([]conversationMessage{}, s.messages...) + writeJSON(w, map[string]any{"messages": messages, "peer": a.peer, "session": a.session, "receivedEvents": s.received, "lastEvent": s.lastEvent}) +} diff --git a/examples/go/web-host/conversation_test.go b/examples/go/web-host/conversation_test.go new file mode 100644 index 00000000..f60d9177 --- /dev/null +++ b/examples/go/web-host/conversation_test.go @@ -0,0 +1,162 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "io" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + "time" + + polymorfa "github.com/polymorfa/sdks/packages/go" +) + +func TestSignedIncomingReplySurvivesReloadAndRejectsOtherTargets(t *testing.T) { + a := &app{peer: "+15550001111", session: "assigned", webhookSecret: "private-local-test-secret", sessions: map[string]appSession{"signed-in": {expires: time.Now().Add(time.Minute), userID: "user"}}} + a.conversation.peerID = "assigned-conversation" + server := httptest.NewServer(a.routes()) + defer server.Close() + post := func(body, signature string) int { + r, _ := http.NewRequest("POST", server.URL+"/webhooks/polymorfa", strings.NewReader(body)) + r.Header.Set("X-Webhook-Signature", signature) + response, err := http.DefaultClient.Do(r) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + return response.StatusCode + } + sign := func(body string) string { + m := hmac.New(sha256.New, []byte(a.webhookSecret)) + m.Write([]byte(body)) + return "sha256=" + hex.EncodeToString(m.Sum(nil)) + } + body := `{"id":"event-1","session":"assigned","timestamp":"2026-10-18T06:00:00Z","event":"message.received","payload":{"id":"incoming-1","conversation":{"id":"assigned-conversation"},"fromMe":false,"timestamp":1792303200,"type":"text","text":"Independent peer reply"}}` + if post(body, "sha256=wrong") != 400 { + t.Fatal("unsigned message accepted") + } + for _, foreign := range []string{strings.Replace(body, `"session":"assigned"`, `"session":"foreign"`, 1), strings.Replace(body, "assigned-conversation", "foreign-conversation", 1)} { + if post(foreign, sign(foreign)) != 204 { + t.Fatal("foreign envelope handling") + } + } + for range 2 { + if post(body, sign(body)) != 204 { + t.Fatal("signed event rejected") + } + } + for range 2 { + r, _ := http.NewRequest("GET", server.URL+"/api/polymorfa/conversation", nil) + r.AddCookie(&http.Cookie{Name: "polymorfa_example_session", Value: "signed-in"}) + response, err := http.DefaultClient.Do(r) + if err != nil { + t.Fatal(err) + } + var snapshot struct { + Messages []conversationMessage + ReceivedEvents int + } + _ = json.NewDecoder(response.Body).Decode(&snapshot) + response.Body.Close() + if response.StatusCode != 200 || len(snapshot.Messages) != 1 || snapshot.Messages[0].Text != "Independent peer reply" || snapshot.ReceivedEvents != 1 { + t.Fatalf("reload/dedup/assignment: %+v", snapshot) + } + } + response, err := http.Get(server.URL + "/api/polymorfa/conversation") + if err != nil { + t.Fatal(err) + } + response.Body.Close() + if response.StatusCode != 401 { + t.Fatal("history exposed without app authentication") + } +} + +func TestNativeSendKeepsCallerKeyAndServerAssignment(t *testing.T) { + var sends atomic.Int32 + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/messaging/assigned/messages/send" || r.Header.Get("Idempotency-Key") != "fbb8bb0d-6943-4101-b893-17bd0aa61199" { + t.Error("native send route or caller key changed") + } + var body struct { + Conversation struct { + Phone string `json:"phoneNumber"` + } + Content struct{ Text string } + } + _ = json.NewDecoder(r.Body).Decode(&body) + if body.Conversation.Phone != "+15550001111" || body.Content.Text != "Hello from Go" { + t.Error("server assignment or text changed") + } + sends.Add(1) + w.Header().Set("Content-Type", "application/json") + io.WriteString(w, `{"success":true,"data":{"id":"receipt-1","whatsapp_ids":{"linked_devices":"wa-1"},"timestamp":"2026-10-18T06:00:00Z","status":"sent","type":"text"}}`) + })) + defer upstream.Close() + sdk, err := polymorfa.NewMessagingClient(polymorfa.Config{Credential: polymorfa.Credential{Kind: polymorfa.OrganizationAPIKey, Value: "pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k"}, BaseURL: upstream.URL}) + if err != nil { + t.Fatal(err) + } + a := &app{client: sdk, username: sha256.Sum256([]byte("user")), password: sha256.Sum256([]byte("password")), session: "assigned", userID: "user", sessions: map[string]appSession{}} + server := httptest.NewServer(a.routes()) + defer server.Close() + a.origin = server.URL + jar, _ := cookiejar.New(nil) + client := &http.Client{Jar: jar, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} + call := func(path, origin, body string) *http.Response { + r, _ := http.NewRequest("POST", server.URL+path, strings.NewReader(body)) + r.Header.Set("Origin", origin) + r.Header.Set("Content-Type", "application/json") + r.Header.Set("Idempotency-Key", "fbb8bb0d-6943-4101-b893-17bd0aa61199") + if path == "/api/login" { + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + response, e := client.Do(r) + if e != nil { + t.Fatal(e) + } + return response + } + r := call("/api/polymorfa/send", server.URL, `{"text":"Hello from Go"}`) + r.Body.Close() + if r.StatusCode != 401 { + t.Fatalf("unauthenticated send: %s", r.Status) + } + r = call("/api/login", server.URL, "username=user&password=password") + r.Body.Close() + // Configure the peer only on the server, after the unauthenticated check. + a.peer = "+15550001111" + r = call("/api/polymorfa/send", "https://foreign.invalid", `{"text":"Hello from Go"}`) + r.Body.Close() + if r.StatusCode != 403 { + t.Fatal("foreign origin send accepted") + } + r = call("/api/polymorfa/send", server.URL, `{"text":"Hello from Go","session":"foreign"}`) + r.Body.Close() + if r.StatusCode != 400 { + t.Fatal("browser target override accepted") + } + for range 2 { + r = call("/api/polymorfa/send", server.URL, `{"text":"Hello from Go"}`) + var result map[string]any + _ = json.NewDecoder(r.Body).Decode(&result) + r.Body.Close() + if r.StatusCode != 200 || result["id"] != "receipt-1" { + t.Fatalf("send: %s %v", r.Status, result) + } + } + if sends.Load() != 1 { + t.Fatal("same caller key sent twice") + } + r = call("/api/polymorfa/send", server.URL, `{"text":"Different text"}`) + r.Body.Close() + if r.StatusCode != 409 || sends.Load() != 1 { + t.Fatal("caller key reused for different content") + } +} diff --git a/examples/go/web-host/go.mod b/examples/go/web-host/go.mod new file mode 100644 index 00000000..40ba2265 --- /dev/null +++ b/examples/go/web-host/go.mod @@ -0,0 +1,57 @@ +module polymorfa-go-web-host + +go 1.24.0 + +require ( + github.com/a-h/templ v0.3.960 + github.com/gin-gonic/gin v1.11.0 + github.com/polymorfa/sdks/packages/go v0.0.0 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.47.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 // indirect + github.com/aws/smithy-go v1.28.5 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/coder/websocket v1.8.15 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) + +replace github.com/polymorfa/sdks/packages/go => ../../../packages/go diff --git a/examples/go/web-host/go.sum b/examples/go/web-host/go.sum new file mode 100644 index 00000000..3dcd9517 --- /dev/null +++ b/examples/go/web-host/go.sum @@ -0,0 +1,114 @@ +github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM= +github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= +github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= +github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3/go.mod h1:GfoBeKMDJecgRUsGf/0RGdx0NJAh81ObTd922h8pQys= +github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k= +github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= +github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/go/web-host/main.go b/examples/go/web-host/main.go new file mode 100644 index 00000000..b8651a08 --- /dev/null +++ b/examples/go/web-host/main.go @@ -0,0 +1,224 @@ +// Loopback example; integrate your application's authentication and live +// membership/number authorization before deploying the host. +package main + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/json" + "errors" + "flag" + "github.com/gin-gonic/gin" + polymorfa "github.com/polymorfa/sdks/packages/go" + "log" + "net/http" + "os" + "strings" + "sync" + "time" +) + +type appSession struct { + expires time.Time + userID string +} +type app struct { + client *polymorfa.MessagingClient + username, password [32]byte + userID, session, origin string + peer string + webhookSecret string + transportLabel string + conversation conversationState + mu sync.Mutex + sessions map[string]appSession +} + +func (a *app) authenticated(r *http.Request) (string, bool) { + cookie, err := r.Cookie("polymorfa_example_session") + if err != nil { + return "", false + } + a.mu.Lock() + defer a.mu.Unlock() + s, ok := a.sessions[cookie.Value] + if !ok || !time.Now().Before(s.expires) { + delete(a.sessions, cookie.Value) + return "", false + } + return s.userID, true +} +func (a *app) routes() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { + _, authenticated := a.authenticated(r) + w.Header().Set("Cache-Control", "no-store") + if err := page(authenticated, a.transportLabel).Render(r.Context(), w); err != nil { + log.Print("Page rendering failed") + } + }) + mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets")))) + mux.HandleFunc("POST /api/login", a.login) + mux.HandleFunc("POST /api/logout", a.logout) + mux.HandleFunc("POST /api/polymorfa/token", a.token) + mux.HandleFunc("POST /api/polymorfa/send", a.sendMessage) + mux.HandleFunc("GET /api/polymorfa/conversation", a.conversationSnapshot) + if a.webhookSecret != "" { + mux.Handle("/webhooks/polymorfa", polymorfa.WebhookHandler(a.webhookSecret, 65536, a.receiveEvent)) + } + mux.HandleFunc("POST /api/polymorfa/connect", func(w http.ResponseWriter, r *http.Request) { + if !a.originAllowed(w, r) { + return + } + if _, ok := a.authenticated(r); !ok { + http.Error(w, "Sign in required.", 401) + return + } + result, err := a.client.QuickLinks().Create(r.Context(), polymorfa.CreateQuickLinkRequest{Purpose: "add_connection", Session: a.session, AddConnection: "linked_devices", ConnectionGoal: "hybrid"}) + if err != nil { + http.Error(w, "Number connection is unavailable.", 502) + return + } + writeJSON(w, result.Data) + }) + return mux +} +func (a *app) originAllowed(w http.ResponseWriter, r *http.Request) bool { + if r.Header.Get("Origin") != a.origin { + http.Error(w, "Invalid request origin.", 403) + return false + } + return true +} +func (a *app) login(w http.ResponseWriter, r *http.Request) { + if !a.originAllowed(w, r) { + return + } + r.Body = http.MaxBytesReader(w, r.Body, 16384) + if err := r.ParseForm(); err != nil { + http.Error(w, "Invalid sign-in request.", 400) + return + } + username, password := sha256.Sum256([]byte(r.Form.Get("username"))), sha256.Sum256([]byte(r.Form.Get("password"))) + if subtle.ConstantTimeCompare(username[:], a.username[:])&subtle.ConstantTimeCompare(password[:], a.password[:]) != 1 { + http.Error(w, "Sign-in failed.", 401) + return + } + token := make([]byte, 32) + if _, err := rand.Read(token); err != nil { + http.Error(w, "Sign-in unavailable.", 503) + return + } + id := base64.RawURLEncoding.EncodeToString(token) + a.mu.Lock() + for key, value := range a.sessions { + if !time.Now().Before(value.expires) { + delete(a.sessions, key) + } + } + if len(a.sessions) >= 128 { + a.mu.Unlock() + http.Error(w, "Too many sessions.", 429) + return + } + a.sessions[id] = appSession{time.Now().Add(30 * time.Minute), a.userID} + a.mu.Unlock() + http.SetCookie(w, &http.Cookie{Name: "polymorfa_example_session", Value: id, Path: "/", HttpOnly: true, SameSite: http.SameSiteStrictMode, MaxAge: 1800, Secure: strings.HasPrefix(a.origin, "https:")}) + a.respondWorkspace(w, r, true) +} +func (a *app) respondWorkspace(w http.ResponseWriter, r *http.Request, authenticated bool) { + w.Header().Set("Cache-Control", "no-store") + if r.Header.Get("HX-Request") == "true" { + if err := workspace(authenticated, a.transportLabel).Render(r.Context(), w); err != nil { + log.Print("Workspace rendering failed") + } + } else { + http.Redirect(w, r, "/", http.StatusSeeOther) + } +} +func (a *app) logout(w http.ResponseWriter, r *http.Request) { + if !a.originAllowed(w, r) { + return + } + if cookie, err := r.Cookie("polymorfa_example_session"); err == nil { + a.mu.Lock() + delete(a.sessions, cookie.Value) + a.mu.Unlock() + } + http.SetCookie(w, &http.Cookie{Name: "polymorfa_example_session", Value: "", Path: "/", MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteStrictMode}) + a.respondWorkspace(w, r, false) +} +func (a *app) token(w http.ResponseWriter, r *http.Request) { + if !a.originAllowed(w, r) { + return + } + user, ok := a.authenticated(r) + if !ok { + http.Error(w, "Sign in required.", 401) + return + } + ttl := 600 + result, err := a.client.ClientTokens().Mint(r.Context(), polymorfa.MintClientTokenRequest{EphemeralID: "example:" + user, Session: a.session, TTLSeconds: &ttl}) + if err != nil { + http.Error(w, "Browser access is unavailable.", 502) + return + } + token := result.Data.Data + expiry, err := time.Parse(time.RFC3339, token.ExpiresAt) + if err != nil || !strings.HasPrefix(token.Token, "pmfa_ct_") { + http.Error(w, "Invalid browser grant response.", 502) + return + } + writeJSON(w, map[string]any{"value": token.Token, "audience": "browser", "expiresAt": expiry.UnixMilli(), "scopes": []string{"read_messages", "connect_whatsapp"}, "grant": map[string]any{"session": a.session, "conversations": []string{a.peer}, "allow": []string{"read_messages", "connect_whatsapp"}}}) +} +func writeJSON(w http.ResponseWriter, value any) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + if err := json.NewEncoder(w).Encode(value); err != nil { + log.Print("Response serialization failed") + } +} +func ginHandler(handler http.Handler) http.Handler { + gin.SetMode(gin.ReleaseMode) + router := gin.New() + router.Use(gin.Recovery()) + _ = router.SetTrustedProxies(nil) + router.Any("/*path", gin.WrapH(handler)) + return router +} +func main() { + mode := flag.String("framework", "nethttp", "nethttp or gin") + port := flag.String("port", "8080", "Loopback port") + flag.Parse() + username, password, session := os.Getenv("EXAMPLE_APP_USERNAME"), os.Getenv("EXAMPLE_APP_PASSWORD"), os.Getenv("POLYMORFA_SESSION") + peer, secret := os.Getenv("POLYMORFA_RECIPIENT"), os.Getenv("POLYMORFA_WEBHOOK_SECRET") + if username == "" || password == "" || session == "" || peer == "" || secret == "" { + log.Fatal("Configure the example account, authorized number, recipient and webhook secret.") + } + credential := polymorfa.Credential{Kind: polymorfa.ProjectToken, Value: os.Getenv("POLYMORFA_PROJECT_TOKEN")} + base := os.Getenv("POLYMORFA_API_BASE_URL") + if base == "" { + base = "https://api.polymorfa.com" + } + client, err := polymorfa.NewMessagingClient(polymorfa.Config{Credential: credential, BaseURL: base}) + if err != nil { + log.Fatal("Invalid API configuration.") + } + a := &app{client: client, username: sha256.Sum256([]byte(username)), password: sha256.Sum256([]byte(password)), session: session, peer: peer, webhookSecret: secret, userID: "local-app-user", origin: "http://127.0.0.1:" + *port, sessions: map[string]appSession{}} + if os.Getenv("POLYMORFA_EXAMPLE_TRANSPORT") == "local-test-number" { + a.transportLabel = "Local test numbers. Simulated WhatsApp transport." + } + handler := a.routes() + if *mode == "gin" { + handler = ginHandler(handler) + } else if *mode != "nethttp" { + log.Fatal("Unknown framework.") + } + server := &http.Server{Addr: "127.0.0.1:" + *port, Handler: handler, ReadHeaderTimeout: 5 * time.Second} + log.Print("Open ", a.origin) + if err = server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatal(err) + } +} diff --git a/examples/go/web-host/main_test.go b/examples/go/web-host/main_test.go new file mode 100644 index 00000000..5bd96f60 --- /dev/null +++ b/examples/go/web-host/main_test.go @@ -0,0 +1,148 @@ +package main + +import ( + "crypto/sha256" + "encoding/json" + "io" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "strings" + "sync/atomic" + "testing" + + polymorfa "github.com/polymorfa/sdks/packages/go" +) + +func TestNativeFrameworkAuthenticatedTokenBoundary(t *testing.T) { + for _, framework := range []string{"nethttp", "gin"} { + t.Run(framework, func(t *testing.T) { + var minted atomic.Int32 + var connected atomic.Int32 + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/messaging/quicklinks" { + if r.Method != "POST" || r.Header.Get("Authorization") != "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k" || r.Header.Get("Polymorfa-Version") != "2026-09-22" { + t.Error("QuickLink upstream auth/version") + } + var body map[string]any + _ = json.NewDecoder(r.Body).Decode(&body) + if len(body) != 4 || body["purpose"] != "add_connection" || body["session"] != "authorized-support" || body["addConnection"] != "linked_devices" || body["connectionGoal"] != "hybrid" { + t.Error("QuickLink server assignment", body) + } + connected.Add(1) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"success":true,"data":{"purpose":"add_connection","connectionGoal":"hybrid","addConnection":"linked_devices","id":"link","url":"https://quick.example.test/link","session":"authorized-support","expiresAt":null}}`)) + return + } + + if r.Method != "POST" || r.URL.Path != "/platform/client-tokens" { + t.Errorf("upstream route %s %s", r.Method, r.URL.Path) + } + if r.Header.Get("Authorization") != "Bearer pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k" || r.Header.Get("Polymorfa-Version") != "2026-09-22" { + t.Error("upstream auth/version") + } + var body map[string]any + _ = json.NewDecoder(r.Body).Decode(&body) + if len(body) != 3 || body["ephemeralId"] != "example:app-user" || body["session"] != "authorized-support" || body["ttlSeconds"] != float64(600) { + t.Error("server assignment", body) + } + minted.Add(1) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"success":true,"data":{"token":"pmfa_ct_YnJvd3Nlcl9maXh0dXJl","expiresAt":"2030-01-01T00:00:00Z"}}`)) + })) + defer upstream.Close() + sdk, err := polymorfa.NewMessagingClient(polymorfa.Config{Credential: polymorfa.Credential{Kind: polymorfa.OrganizationAPIKey, Value: "pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k"}, BaseURL: upstream.URL}) + if err != nil { + t.Fatal(err) + } + a := &app{client: sdk, username: sha256.Sum256([]byte("user")), password: sha256.Sum256([]byte("test-password")), userID: "app-user", session: "authorized-support", sessions: map[string]appSession{}} + handler := a.routes() + if framework == "gin" { + handler = ginHandler(handler) + } + server := httptest.NewServer(handler) + defer server.Close() + a.origin = server.URL + jar, _ := cookiejar.New(nil) + client := &http.Client{Jar: jar, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }} + send := func(method, path, origin, body string) *http.Response { + r, _ := http.NewRequest(method, server.URL+path, strings.NewReader(body)) + r.Header.Set("Origin", origin) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + r.Header.Set("HX-Request", "true") + response, err := client.Do(r) + if err != nil { + t.Fatal(err) + } + return response + } + response := send("POST", "/api/polymorfa/token", server.URL, "") + if response.StatusCode != 401 { + t.Fatal(response.Status) + } + response.Body.Close() + response = send("GET", "/api/login", server.URL, "") + if response.StatusCode != 405 { + t.Fatal("GET login", response.Status) + } + response.Body.Close() + response = send("POST", "/api/login", "https://foreign.invalid", "username=user&password=test-password") + if response.StatusCode != 403 { + t.Fatal(response.Status) + } + response.Body.Close() + response = send("POST", "/api/login", server.URL, "username=user&password=wrong") + if response.StatusCode != 401 { + t.Fatal(response.Status) + } + response.Body.Close() + response = send("POST", "/api/login", server.URL, url.Values{"username": {"user"}, "password": {"test-password"}}.Encode()) + html, _ := io.ReadAll(response.Body) + response.Body.Close() + if response.StatusCode != 200 || !strings.Contains(string(html), "pmfa-inbox") { + t.Fatal("templ login", response.Status, string(html)) + } + cookies := response.Cookies() + if len(cookies) != 1 || !cookies[0].HttpOnly || cookies[0].SameSite != http.SameSiteStrictMode || strings.Contains(cookies[0].Value, "app-user") { + t.Fatal("opaque application session") + } + response = send("POST", "/api/polymorfa/token", server.URL, "session=attacker&ephemeralId=attacker") + var token map[string]any + _ = json.NewDecoder(response.Body).Decode(&token) + response.Body.Close() + if response.StatusCode != 200 || token["audience"] != "browser" || token["value"] != "pmfa_ct_YnJvd3Nlcl9maXh0dXJl" || response.Header.Get("Cache-Control") != "no-store" || minted.Load() != 1 { + t.Fatal("token result", token, response.Status) + } + response = send("POST", "/api/polymorfa/connect", server.URL, "session=attacker&purpose=initial") + var link map[string]any + _ = json.NewDecoder(response.Body).Decode(&link) + response.Body.Close() + if response.StatusCode != 200 || connected.Load() != 1 || link["data"].(map[string]any)["url"] != "https://quick.example.test/link" { + t.Fatal("supplementary QuickLink", response.Status, link) + } + response = send("POST", "/api/polymorfa/connect", "https://foreign.invalid", "") + if response.StatusCode != 403 || connected.Load() != 1 { + t.Fatal("QuickLink origin bypass") + } + response.Body.Close() + response = send("POST", "/api/polymorfa/token", "https://foreign.invalid", "") + if response.StatusCode != 403 || minted.Load() != 1 { + t.Fatal("origin bypass") + } + response.Body.Close() + response = send("POST", "/api/logout", server.URL, "") + response.Body.Close() + response = send("POST", "/api/polymorfa/token", server.URL, "") + if response.StatusCode != 401 || minted.Load() != 1 { + t.Fatal("logout failed") + } + response.Body.Close() + response = send("POST", "/api/polymorfa/connect", server.URL, "") + if response.StatusCode != 401 || connected.Load() != 1 { + t.Fatal("QuickLink logout bypass") + } + response.Body.Close() + }) + } +} diff --git a/examples/go/web-host/page.templ b/examples/go/web-host/page.templ new file mode 100644 index 00000000..ce2f6bb8 --- /dev/null +++ b/examples/go/web-host/page.templ @@ -0,0 +1,30 @@ +package main + +templ page(authenticated bool, transportLabel string) { + + + Go Polymorfa workspace + + @workspace(authenticated, transportLabel) +} + +templ workspace(authenticated bool, transportLabel string) { +

Messaging workspace

+ if authenticated { + if transportLabel != "" { +

{ transportLabel }

+ } +

Go SDK. Text messages use the server-assigned number and recipient. Conversation state lasts until this example server stops.

+

Waiting for the native SDK conversation.

+
+ + } else { +

Sign in with the local example application's configured account.

+
+ + + +
+ } +
+} diff --git a/examples/go/web-host/page_templ.go b/examples/go/web-host/page_templ.go new file mode 100644 index 00000000..b5f11971 --- /dev/null +++ b/examples/go/web-host/page_templ.go @@ -0,0 +1,111 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.960 +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +func page(authenticated bool, transportLabel string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Go Polymorfa workspace") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = workspace(authenticated, transportLabel).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func workspace(authenticated bool, transportLabel string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

Messaging workspace

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if authenticated { + if transportLabel != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(transportLabel) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `page.templ`, Line: 15, Col: 30} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

Go SDK. Text messages use the server-assigned number and recipient. Conversation state lasts until this example server stops.

Waiting for the native SDK conversation.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

Sign in with the local example application's configured account.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/examples/go/webhook-server/go.mod b/examples/go/webhook-server/go.mod new file mode 100644 index 00000000..6ead872d --- /dev/null +++ b/examples/go/webhook-server/go.mod @@ -0,0 +1,9 @@ +module polymorfa-go-webhook-server + +go 1.24.0 + +require github.com/polymorfa/sdks/packages/go v0.0.0 + +require github.com/coder/websocket v1.8.15 // indirect + +replace github.com/polymorfa/sdks/packages/go => ../../../packages/go diff --git a/examples/go/webhook-server/go.sum b/examples/go/webhook-server/go.sum new file mode 100644 index 00000000..4e0a48d6 --- /dev/null +++ b/examples/go/webhook-server/go.sum @@ -0,0 +1,2 @@ +github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= +github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= diff --git a/examples/go/webhook-server/main.go b/examples/go/webhook-server/main.go new file mode 100644 index 00000000..cf68a425 --- /dev/null +++ b/examples/go/webhook-server/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + polymorfa "github.com/polymorfa/sdks/packages/go" + "log" + "net/http" + "os" + "time" +) + +func main() { + secret := os.Getenv("POLYMORFA_WEBHOOK_SECRET") + if secret == "" { + log.Fatal("Set POLYMORFA_WEBHOOK_SECRET.") + } + handler := polymorfa.WebhookHandler(secret, 1<<20, func(r *http.Request, e polymorfa.WebhookEvent) error { + payload, known, err := e.TypedPayload() + if err != nil { + return err + } + fmt.Printf("Verified event %s: %s (%T, known=%t)\n", e.ID, e.Event, payload, known) + return nil + }) + server := &http.Server{Addr: "127.0.0.1:8080", Handler: handler, ReadHeaderTimeout: 5 * time.Second} + log.Fatal(server.ListenAndServe()) +} diff --git a/examples/official-api-round2/Catalogs.cs b/examples/official-api-round2/Catalogs.cs new file mode 100644 index 00000000..280d775b --- /dev/null +++ b/examples/official-api-round2/Catalogs.cs @@ -0,0 +1,10 @@ +using Polymorfa.Sdk; + +public static class OfficialCatalogExample +{ + public static async Task NextCatalogCursorAsync(MessagingClient client, CancellationToken cancellationToken) + { + var page = await client.Catalogs.ListProductsAsync("number-id", "123", new(100, "opaque_-"), new() { CancellationToken = cancellationToken }); + return page.Data.Data.NextCursor; + } +} diff --git a/examples/official-api-round2/catalogs.go b/examples/official-api-round2/catalogs.go new file mode 100644 index 00000000..a61db587 --- /dev/null +++ b/examples/official-api-round2/catalogs.go @@ -0,0 +1,16 @@ +package officialround2 + +import ( + "context" + polymorfa "github.com/polymorfa/sdks/packages/go" +) + +// NextCatalogCursor uses an existing authenticated server client. +func NextCatalogCursor(ctx context.Context, client *polymorfa.MessagingClient) (string, error) { + limit := 100 + page, err := client.Catalogs().ListProducts(ctx, "number-id", "123", polymorfa.CatalogProductsParams{Limit: &limit, Cursor: "opaque_-"}) + if err != nil { + return "", err + } + return page.Data.Data.NextCursor, nil +} diff --git a/examples/official-api-round2/catalogs.php b/examples/official-api-round2/catalogs.php new file mode 100644 index 00000000..e9d2c137 --- /dev/null +++ b/examples/official-api-round2/catalogs.php @@ -0,0 +1,9 @@ +catalogs->listProducts('number-id', '123', ['limit' => 100, 'cursor' => 'opaque_-']); + return $page->data['data']['nextCursor'] ?? null; +} diff --git a/examples/official-api-round2/catalogs.py b/examples/official-api-round2/catalogs.py new file mode 100644 index 00000000..56294357 --- /dev/null +++ b/examples/official-api-round2/catalogs.py @@ -0,0 +1,5 @@ +from polymorfa import AsyncMessagingClient + +async def next_catalog_cursor(client: AsyncMessagingClient) -> str | None: + page = await client.catalogs.list_products("number-id", "123", limit=100, cursor="opaque_-") + return page.data["data"].get("nextCursor") diff --git a/examples/official-api-round2/catalogs.rs b/examples/official-api-round2/catalogs.rs new file mode 100644 index 00000000..9ddfaf74 --- /dev/null +++ b/examples/official-api-round2/catalogs.rs @@ -0,0 +1,19 @@ +use polymorfa_sdk::{ + official_round2::CatalogProductsParameters, MessagingClient, RequestOptions, Result, +}; + +pub async fn next_catalog_cursor(client: &MessagingClient) -> Result> { + let page = client + .catalogs() + .list_products( + "number-id", + "123", + &CatalogProductsParameters { + limit: Some(100), + cursor: Some("opaque_-".into()), + }, + RequestOptions::default(), + ) + .await?; + Ok(page.data.data.next_cursor) +} diff --git a/examples/official-api-round2/index.ts b/examples/official-api-round2/index.ts new file mode 100644 index 00000000..292c1ddc --- /dev/null +++ b/examples/official-api-round2/index.ts @@ -0,0 +1,89 @@ +import { + Client, + MessagingClient, + PolymorfaError, + constructWebhookDelivery, + isWebhookEventBatch, + isEvent, + type WebhookBody, +} from "@polymorfa/sdk"; + +/** Prepared for enrolled betas. Supply a regional Messaging base URL and a + * project token issued for that region; this example never mints credentials. + */ +export async function inspectNumber( + messaging: MessagingClient, + number: string, +) { + try { + const catalogs = await messaging.catalogs.list(number); + const marketing = await messaging.cloudMarketing.getStatus(number); + const history = await messaging.chats.list(number, { kind: "group" }); + return { + catalogs: catalogs.data.data, + marketing: marketing.data.data, + history: history.data.data, + }; + } catch (error) { + if (error instanceof PolymorfaError && error.code === "feature_unavailable") + return { unavailable: true }; + throw error; + } +} + +export async function refreshCatalog( + messaging: MessagingClient, + number: string, + catalog: string, +) { + await messaging.catalogs.sync(number, catalog); + return messaging.catalogs.listProducts(number, catalog, { limit: 50 }); +} + +/** Call only after the receiver handles single deliveries and batches. */ +export function enableBatching( + client: Client, + webhookId: string, + idempotencyKey: string, +) { + return client.webhooks.update( + webhookId, + { + format: "native", + batching: { enabled: true, maxEvents: 100, windowMs: 1000 }, + }, + { idempotencyKey }, + ); +} + +/** Verify once over the whole raw request, then deduplicate member IDs in + * durable receiver storage before effects. Return 2xx only after every member + * is durably handled; retries preserve the batch ID and individual event IDs. + */ +export async function receive( + raw: WebhookBody, + signature: string, + secret: string, + once: (eventId: string, effect: () => Promise) => Promise, +) { + const delivery = await constructWebhookDelivery(raw, signature, secret); + const events = isWebhookEventBatch(delivery) ? delivery.events : [delivery]; + for (const event of events) { + await once(event.id, async () => { + if ( + isEvent(event, "message.received") && + event.payload.order !== undefined + ) { + // Read the validated cart; it carries no computed total or payment status. + console.log(event.payload.order.items.length); + } + if ( + isEvent(event, "contact.update") && + event.payload.identityChange !== undefined + ) { + // Keep previous and new identities separate. parentBsuid is metadata only. + console.log(event.payload.identityChange.reason); + } + }); + } +} diff --git a/examples/php/README.md b/examples/php/README.md new file mode 100644 index 00000000..636bb6e0 --- /dev/null +++ b/examples/php/README.md @@ -0,0 +1,7 @@ +# PHP server examples + +Run `composer install` inside `packages/php`, then run `php examples/php/send.php` from the repository root. Set `POLYMORFA_API_KEY`, `POLYMORFA_SESSION`, `POLYMORFA_RECIPIENT`, and `POLYMORFA_IDEMPOTENCY_KEY` in your server environment. This source SDK has not been published to Packagist. The send example makes a real authorized API request. + +The Laravel and Symfony snippets verify original request content with `WebhookRequest` before decoding JSON. Configure the signing secret in your application's secret store. Add durable deduplication by the verified event ID before processing. Unknown event payloads remain available. + +`calls-media.php` requires `phrity/websocket` and attaches to an already admitted Call. Set the Call ID, connection ID and authorized participant environment variables named in its source. Server credentials travel in the first WebSocket frame; the URL contains only the Call ID. This synchronous example receives PCM. Applications must drive control messages and media receive calls while connected. diff --git a/examples/php/calls-media.php b/examples/php/calls-media.php new file mode 100644 index 00000000..d52b0505 --- /dev/null +++ b/examples/php/calls-media.php @@ -0,0 +1,16 @@ +connect();$frame=$socket->receive(); + if($frame instanceof AudioFrame) { echo count($frame->samples)." PCM samples received\n"; } + $socket->leave(); +} finally { $socket->close(); } diff --git a/examples/php/laravel-webhook.php b/examples/php/laravel-webhook.php new file mode 100644 index 00000000..f52edee3 --- /dev/null +++ b/examples/php/laravel-webhook.php @@ -0,0 +1,20 @@ +json(['error' => $error->errorCode], $error->status ?? 400); + } catch (WebhookSignatureException) { + return response()->json(['error' => 'invalid_signature'], 400); + } + // Persist event.id for delivery deduplication before processing its payload. + return response()->json(['received' => $event->id]); +}); diff --git a/examples/php/send.php b/examples/php/send.php new file mode 100644 index 00000000..d40b74de --- /dev/null +++ b/examples/php/send.php @@ -0,0 +1,14 @@ +messages->send((string)getenv('POLYMORFA_SESSION'),[ + 'conversation'=>['phoneNumber'=>(string)getenv('POLYMORFA_RECIPIENT')], + 'content'=>['text'=>'Hello from PHP'], +],new RequestOptions(idempotencyKey:(string)getenv('POLYMORFA_IDEMPOTENCY_KEY'))); +echo $response->data['data']['id']."\n"; diff --git a/examples/php/symfony-webhook.php b/examples/php/symfony-webhook.php new file mode 100644 index 00000000..f0653cc7 --- /dev/null +++ b/examples/php/symfony-webhook.php @@ -0,0 +1,22 @@ +getMethod() !== 'POST') { + return new JsonResponse(['error' => 'method_not_allowed'], 405, ['Allow' => 'POST']); + } + try { + $event = WebhookRequest::fromSymfony($request, $signingSecret, $maxBodyBytes); + } catch (ValidationException $error) { + return new JsonResponse(['error' => $error->errorCode], $error->status ?? 400); + } catch (WebhookSignatureException) { + return new JsonResponse(['error' => 'invalid_signature'], 400); + } + return new JsonResponse(['received' => $event->id]); +} diff --git a/examples/php/web-host/.gitignore b/examples/php/web-host/.gitignore new file mode 100644 index 00000000..c1247d94 --- /dev/null +++ b/examples/php/web-host/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +/public/assets/ +/bootstrap/cache/ diff --git a/examples/php/web-host/README.md b/examples/php/web-host/README.md new file mode 100644 index 00000000..dae47c6a --- /dev/null +++ b/examples/php/web-host/README.md @@ -0,0 +1,50 @@ +# PHP web hosts + +Run Laravel with Blade or Symfony HttpKernel with Twig. Both hosts use the PHP server SDK and mount the existing Polymorfa Web Components. HTMX submits the sign-in and sign-out forms. + +From the repository root: + +```sh +npm ci +node examples/php/web-host/build-assets.mjs +cd examples/php/web-host +composer install +php -S 127.0.0.1:8080 -t public public/router.php +``` + +Before starting PHP, configure `EXAMPLE_APP_USERNAME`, `EXAMPLE_APP_PASSWORD`, `POLYMORFA_PROJECT_TOKEN`, `POLYMORFA_SESSION`, `EXAMPLE_APP_ORIGIN=http://127.0.0.1:8080` and an absolute `EXAMPLE_STATE_DIR` under a persistent private application directory. The host creates private session and template-cache directories there. Open the configured origin. `EXAMPLE_FRAMEWORK=laravel` is the default; select `symfony` for HttpKernel and Twig. `POLYMORFA_API_BASE_URL` optionally selects the API origin. Authenticated plaintext API URLs are confined to loopback. + +These are loopback demonstration hosts with one configured application account. Sign-in creates an opaque random session, retained in a private file for 30 minutes, and an HttpOnly, SameSite=Strict cookie. POST routes enforce the configured application origin. The authenticated token endpoint derives the user and authorized number on the server, mints a ten-minute client token through `clientTokens->mint`, and returns browser claims with `Cache-Control: no-store, private`. Browser request fields cannot select the user or number. Sign-out removes the stored session and disposes the browser client. The inbox uses the native server SDK; the separate token endpoint remains an optional browser-grant example. + +Before public hosting, use the application's real authentication, live membership and number authorization, HTTPS origin, managed sessions and CSRF protection appropriate to the application. Keep project and organization credentials exclusively on the server. + +Laravel uses its real HTTP application and Blade view service; Symfony uses HttpKernel, HttpFoundation and Twig. Their dependencies and Composer lock file are separate from the SDK package. Blade caches and sessions remain outside the source checkout. Asset building bundles the workspace Web Components and downloads pinned HTMX 2.0.4 after verifying its SHA-256 digest. + +Run the native HTTP tests with PHP 8.2 or newer: + +```sh +EXAMPLE_STATE_DIR="$HOME/.polymorfa-agent-work/php-web-host-tests" php tests/run.php +``` + +The test starts loopback servers on ports 31305 and 31306 with synthetic credentials. It verifies both frameworks, initial template rendering, POST-only sign-in, invalid credentials, foreign origins, opaque cookies, exact server-derived SDK mint requests, browser response claims and revocation after sign-out. PHP 8.2 and 8.4 passed these tests. Earlier browser smoke checks covered sign-in and an empty inbox. Functional video acceptance is recorded separately below. No customer credentials or live WhatsApp number were used. + +The Connect number endpoint creates a hosted supplementary linked-device invitation for the server-assigned `POLYMORFA_SESSION`. Assign an eligible Official Number that can add linked devices. The API still checks assignment, eligibility, consent and funding. Browser fields cannot choose another Number or change custody. This does not create an initial Number or establish live WhatsApp pairing. + +The inbox now calls the native PHP SDK for text sends and consumes signed native +webhooks at `/webhooks/polymorfa`. Configure `POLYMORFA_RECIPIENT` and +`POLYMORFA_WEBHOOK_SECRET` on the server; register `message.received` and +`message.ack` for the assigned Number with the same secret. The browser cannot +choose a different target. Native signatures are checked before parsing and +updates are confined to the assigned Number and canonical conversation ID. + +The private `EXAMPLE_STATE_DIR` keeps at most 100 messages, 128 send attempts and +1024 recent event IDs. Clear this directory explicitly to reset the example. It +is application-owned state and does not enable HMS. Sends retain their UUID key +on unknown outcomes; retry the same text and key. Display timestamps use whole +seconds, matching native message events. Laravel and Symfony share these handlers. +The inbox does not use the separate token/connect examples. + +Set `POLYMORFA_EXAMPLE_TRANSPORT=local-test-number` to label simulated transport. +The local Inspector video acceptance checks native sends, an independent paired peer's +reply, signed HTTPS delivery, reload and logout. It does not prove registry, +staging or production availability. diff --git a/examples/php/web-host/bootstrap/laravel.php b/examples/php/web-host/bootstrap/laravel.php new file mode 100644 index 00000000..249cdb8b --- /dev/null +++ b/examples/php/web-host/bootstrap/laravel.php @@ -0,0 +1,37 @@ +withRouting(using:function (): void { + Route::any('/{path?}', function (Illuminate\Http\Request $request): Response { + $host = new Host(fn (bool $authenticated, bool $partial, bool $localTest): string => view('workspace', compact('authenticated', 'partial', 'localTest'))->render()); + return $host->handle($request); + })->where('path', '.*'); + }) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->use([]); + }) + ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->report(function (Throwable $error): bool { + return false; + }); + $exceptions->render(fn (Throwable $error) => new Response('Example application unavailable.', 503, ['Cache-Control' => 'no-store'])); + })->create(); +$application->useStoragePath($state); +$application->useBootstrapPath($state.'/bootstrap'); +Host::directory($state.'/bootstrap/cache'); +return $application; diff --git a/examples/php/web-host/bridge.js b/examples/php/web-host/bridge.js new file mode 100644 index 00000000..d5d92ef5 --- /dev/null +++ b/examples/php/web-host/bridge.js @@ -0,0 +1,150 @@ +import { definePolymorfaElements } from "@polymorfa/elements"; +definePolymorfaElements(); +let disposeInbox; +// Only receipts the host observed from message.ack become chat receipts. The +// send response status is upstream acceptance, never delivery or read. +const observedReceipts = new Map([ + ["delivered", "delivered"], + ["delivery", "delivered"], + ["read", "read"], + ["played", "played"], +]); +function chatMessage({ receipt, ...message }) { + const state = + message.direction === "outbound" + ? observedReceipts.get(receipt) + : undefined; + return state ? { ...message, receipt: { state } } : message; +} +// The browser renders shared components. Every send and incoming message is +// handled by this application's native PHP SDK, not the browser API transport. +function nativeInbox(inbox) { + const changes = new Set(); + const messages = new Set(); + let previous = ""; + let stopped = false; + let timer; + const request = async (path, options = {}) => { + const response = await fetch(`/api/polymorfa/${path}`, { + credentials: "same-origin", + ...options, + }); + if (!response.ok) throw new Error(await response.text()); + return response.json(); + }; + const row = (snapshot) => { + const last = snapshot.messages.at(-1); + return { + id: snapshot.peer, + session: snapshot.session, + name: "Assigned recipient", + phoneNumber: snapshot.peer, + lastActivity: last?.createdAt ?? 0, + unreadCount: 0, + ...(last + ? { + lastMessage: { + text: last.text, + createdAt: last.createdAt, + direction: last.direction, + }, + } + : {}), + }; + }; + const poll = async () => { + try { + const snapshot = await request("conversation"); + if (stopped) return; + const serialized = JSON.stringify(snapshot); + if (serialized !== previous) { + previous = serialized; + for (const notify of changes) + notify({ type: "conversation", conversation: row(snapshot) }); + for (const message of snapshot.messages) + for (const notify of messages) + notify({ type: "upsert", message: chatMessage(message) }); + } + const status = document.querySelector("#native-status"); + if (status) { + const outgoing = snapshot.messages.findLast( + (m) => m.direction === "outbound", + ); + status.textContent = + `PHP SDK · ${snapshot.receivedEvents} verified webhook events` + + (outgoing ? ` · Latest send: ${outgoing.receipt}` : ""); + } + } catch (error) { + const status = document.querySelector("#native-status"); + if (status && !stopped) status.textContent = error.message; + } finally { + if (!stopped) timer = setTimeout(poll, 800); + } + }; + inbox.source = { + async listConversations(_cursor, signal) { + return { + conversations: [row(await request("conversation", { signal }))], + }; + }, + subscribe(notify) { + changes.add(notify); + return () => changes.delete(notify); + }, + conversation() { + return { + async load(_cursor, signal) { + return { + messages: (await request("conversation", { signal })).messages.map( + chatMessage, + ), + }; + }, + subscribe(notify) { + messages.add(notify); + return () => messages.delete(notify); + }, + async send(message, signal) { + if (message.replyTo || message.attachments?.length) + throw new Error("This native example sends text only."); + return chatMessage( + await request("send", { + method: "POST", + signal, + headers: { + "Content-Type": "application/json", + "Idempotency-Key": message.clientId, + }, + body: JSON.stringify({ text: message.text }), + }), + ); + }, + }; + }, + }; + void poll(); + return () => { + stopped = true; + clearTimeout(timer); + changes.clear(); + messages.clear(); + }; +} +function mount() { + const inbox = document.querySelector("pmfa-inbox"); + if (inbox) { + disposeInbox ??= nativeInbox(inbox); + } else { + disposeInbox?.(); + disposeInbox = undefined; + } +} +document.addEventListener("htmx:afterSwap", mount); +document.addEventListener("htmx:responseError", () => { + const error = document.querySelector("#error"); + if (error) + error.textContent = "The request failed. Check your sign-in and try again."; +}); +if (document.readyState === "loading") + document.addEventListener("DOMContentLoaded", mount, { once: true }); +else mount(); diff --git a/examples/php/web-host/build-assets.mjs b/examples/php/web-host/build-assets.mjs new file mode 100644 index 00000000..91d3f399 --- /dev/null +++ b/examples/php/web-host/build-assets.mjs @@ -0,0 +1,24 @@ +import { build } from "esbuild"; +import { createHash } from "node:crypto"; +import { writeFile, mkdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +await mkdir(new URL("public/assets/", import.meta.url), { recursive: true }); +await build({ + entryPoints: [fileURLToPath(new URL("bridge.js", import.meta.url))], + outfile: fileURLToPath(new URL("public/assets/elements.js", import.meta.url)), + bundle: true, + format: "esm", + platform: "browser", + target: "es2022", +}); +const response = await fetch( + "https://raw.githubusercontent.com/bigskysoftware/htmx/v2.0.4/dist/htmx.min.js", +); +if (!response.ok) throw new Error("Cannot fetch pinned HTMX asset."); +const body = Buffer.from(await response.arrayBuffer()); +if ( + createHash("sha256").update(body).digest("hex") !== + "e209dda5c8235479f3166defc7750e1dbcd5a5c1808b7792fc2e6733768fb447" +) + throw new Error("Pinned HTMX integrity check failed."); +await writeFile(new URL("public/assets/htmx.js", import.meta.url), body); diff --git a/examples/php/web-host/composer.json b/examples/php/web-host/composer.json new file mode 100644 index 00000000..e58c7745 --- /dev/null +++ b/examples/php/web-host/composer.json @@ -0,0 +1,28 @@ +{ + "name": "polymorfa/example-web-host", + "description": "Laravel Blade and Symfony hosts for existing Polymorfa Web Components", + "type": "project", + "require": { + "php": ">=8.2", + "polymorfa/sdk": "@dev", + "laravel/framework": "^12.0", + "symfony/http-kernel": "^7.4", + "twig/twig": "^3.0" + }, + "repositories": [ + { + "type": "path", + "url": "../../../packages/php", + "options": { + "symlink": true + } + } + ], + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "PolymorfaExample\\": "src/" + } + } +} diff --git a/examples/php/web-host/composer.lock b/examples/php/web-host/composer.lock new file mode 100644 index 00000000..c3e6db0e --- /dev/null +++ b/examples/php/web-host/composer.lock @@ -0,0 +1,5987 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "c3f78b31051c699c8c5789cc9d1ce40b", + "packages": [ + { + "name": "brick/math", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-02-10T14:33:43+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc", + "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2026-09-06T14:11:38+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/71d305eaa6480d3d3460c7d053d1da7749fa9cca", + "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10.5.58 || ^12.5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2026-10-01T06:59:29+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/adccca3324eece92ca35463648c12b9e6293c05b", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.10" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.52 || ^9.6.34 || ^10.5.63 || ^11.5.55 || ^12.5.14" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:06:52+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.15.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee80339fd9177ba44c49cdb653ff02a4d1106b9a", + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.5.3", + "guzzlehttp/psr7": "^2.13.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.7", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.15.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:21:06+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.5.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/cde49999552d185d64715fe9c1f77a2aadd2f9f1", + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.5.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:11:28+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.13.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "95e7828100de18b4e269fb1703be530082d5166d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/95e7828100de18b4e269fb1703be530082d5166d", + "reference": "95e7828100de18b4e269fb1703be530082d5166d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.13.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:13:11+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.11", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "d0058dccf4299d70c3d9da3378b8908b32780368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/d0058dccf4299d70c3d9da3378b8908b32780368", + "reference": "d0058dccf4299d70c3d9da3378b8908b32780368", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.25" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.11" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:15:32+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.69.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/58ea544a2a80dc03c168e13a5dc9a1d176a88717", + "reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.34", + "symfony/polyfill-php85": "^1.34", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.1.41", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-09-29T12:53:25+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.25", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "21b7b49d60fcb700016dd27b582ae739c75de816" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/21b7b49d60fcb700016dd27b582ae739c75de816", + "reference": "21b7b49d60fcb700016dd27b582ae739c75de816", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "source": "https://github.com/laravel/prompts/tree/v0.3.25" + }, + "time": "2026-10-06T02:25:49+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2d5869a838bbcf37e0d8b0568fc41914e81374b5", + "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-09-22T14:32:34+00:00" + }, + { + "name": "league/commonmark", + "version": "2.10.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6efbd9c472b91db0a3350fcd601c8332c2382e1f", + "reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.11-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-09-21T13:07:34+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.36.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f7fb152932f30072d573510cbd4dd657d6475b25", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.36.0" + }, + "time": "2026-09-02T08:00:27+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.35.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/a099b24dce160f3b2239043d13d47c4a1a214ea4", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.35.3" + }, + "time": "2026-08-12T13:29:21+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.17.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2026-07-09T11:49:27+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.12.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/d7059e4c691d05ea1222b5124bed0f961be46e15", + "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "psr/clock": "^1.0", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.12.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-09-29T19:21:12+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.14.2", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "94cfc34887f07ab5c0efa07914c8f527544e3224" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/94cfc34887f07ab5c0efa07914c8f527544e3224", + "reference": "94cfc34887f07ab5c0efa07914c8f527544e3224", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^3.0@dev", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-10-02T07:26:52+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.6", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "c54350438cd6914616f790a49cb424605f421562" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/c54350438cd6914616f790a49cb424605f421562", + "reference": "c54350438cd6914616f790a49cb424605f421562", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.6" + }, + "time": "2026-08-16T21:58:41+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.5", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::chr(), ord() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.5" + }, + "time": "2026-07-17T23:02:45+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/67b192b6a42ec03944b972d6e633ddec78ad2c6d", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.54 || ^9.6.36 || ^10.5.64 || ^11.5.56 || ^12.5.33" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2026-08-24T00:54:40+00:00" + }, + { + "name": "polymorfa/sdk", + "version": "dev-main", + "dist": { + "type": "path", + "url": "../../../packages/php", + "reference": "80dda1d189a56d3ceff075d1c3fa618897a86567" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^7.9", + "php": ">=8.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.75", + "illuminate/http": "^12.0", + "phpstan/phpstan": "^2.1", + "phrity/websocket": "^4.0", + "symfony/http-foundation": "^7.0" + }, + "suggest": { + "illuminate/http": "Use the Laravel raw-body webhook adapter.", + "phrity/websocket": "Use native server Calls WebSocket transport.", + "symfony/http-foundation": "Use the Symfony raw-body webhook adapter." + }, + "type": "library", + "autoload": { + "psr-4": { + "Polymorfa\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Polymorfa\\Tests\\": "tests/" + } + }, + "scripts": { + "lint": [ + "php tools/lint.php" + ], + "format": [ + "php-cs-fixer fix --config=.php-cs-fixer.dist.php" + ], + "format:check": [ + "php-cs-fixer fix --config=.php-cs-fixer.dist.php --dry-run --diff" + ], + "test": [ + "php tests/run.php" + ], + "typecheck": [ + "phpstan analyse --configuration=phpstan.neon --no-progress" + ], + "build": [ + "php tools/build.php" + ] + }, + "license": [ + "MIT" + ], + "description": "Handwritten Polymorfa server SDK for PHP", + "transport-options": { + "symlink": true, + "relative": true + } + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.4", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/75d73f48d02797c2c285a7e9f348fadc0102ffe2", + "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.4" + }, + "time": "2026-09-16T11:39:30+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", + "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-18T15:55:00+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "fecf40067fc8d8880ea87b8ac227600b9aadd1d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/fecf40067fc8d8880ea87b8ac227600b9aadd1d0", + "reference": "fecf40067fc8d8880ea87b8ac227600b9aadd1d0", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-23T10:03:40+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0", + "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-22T18:56:40+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d269974ee93c61d03620ffee358355bfdb471d66", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-26T06:31:10+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "9d0761c0da6bd5b801b401666834901a9a1ffc33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9d0761c0da6bd5b801b401666834901a9a1ffc33", + "reference": "9d0761c0da6bd5b801b401666834901a9a1ffc33", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T07:12:44+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "392784b5d7b959b8d872542a2530c50c540a788f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/392784b5d7b959b8d872542a2530c50c540a788f", + "reference": "392784b5d7b959b8d872542a2530c50c540a788f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-29T14:05:16+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "6f6f2441bef07a42d2617b9b3aadf04a321aa514" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/6f6f2441bef07a42d2617b9b3aadf04a321aa514", + "reference": "6f6f2441bef07a42d2617b9b3aadf04a321aa514", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-15T06:01:15+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.44|^7.4.17|^8.1.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-04T10:45:44+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/533d68cf99c8f8ee2e8893c59ed59192e92cfd74", + "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T14:40:04+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T22:02:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T15:19:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/80ccff923a8d61f73ebe9da0aa94e25232f004ce", + "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/22c6548499cbe5c5bb072fc0c42312dff062a015", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T22:01:09+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/86a42cbcc07adc331481a5ab310a4024bfc227f6", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "b23ff8245cc5c4479ea2be3d0132d2e6dd433993" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/b23ff8245cc5c4479ea2be3d0132d2e6dd433993", + "reference": "b23ff8245cc5c4479ea2be3d0132d2e6dd433993", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T05:20:52+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "ed0ae095b86994d370d5791612e55984f15aa30e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/ed0ae095b86994d370d5791612e55984f15aa30e", + "reference": "ed0ae095b86994d370d5791612e55984f15aa30e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-02T12:38:34+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "2bacb72be5eadeaf9874d5accb75bfb33d21c74b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/2bacb72be5eadeaf9874d5accb75bfb33d21c74b", + "reference": "2bacb72be5eadeaf9874d5accb75bfb33d21c74b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-28T19:09:50+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-27T15:39:01+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "02731ce2bd26514bec9efc0baef2cd0b0cf67f46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/02731ce2bd26514bec9efc0baef2cd0b0cf67f46", + "reference": "02731ce2bd26514bec9efc0baef2cd0b0cf67f46", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-11T14:46:58+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/2ee1e4a3b32a528a642babe041ff7c440b213b4b", + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "a96bbdb02a04ff3b5592cee6741b03edc78b56aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/a96bbdb02a04ff3b5592cee6741b03edc78b56aa", + "reference": "a96bbdb02a04ff3b5592cee6741b03edc78b56aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T16:18:28+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "e088da50b813f32473a76871616cbb8fa54653a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e088da50b813f32473a76871616cbb8fa54653a8", + "reference": "e088da50b813f32473a76871616cbb8fa54653a8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-30T20:10:52+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "twig/twig", + "version": "v3.30.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/8c737079b726af72ff8ef3c595be9f6a810ea1ef", + "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.30.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-09-25T13:20:01+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/phpdotenv/phpdotenv.git", + "reference": "301c07936b16d88628b126b01d082ba153cf4c40" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpdotenv/phpdotenv/zipball/301c07936b16d88628b126b01d082ba153cf4c40", + "reference": "301c07936b16d88628b126b01d082ba153cf4c40", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.2", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.10", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `$_ENV` and `$_SERVER` automagically, and optionally to `getenv()`.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2026-08-24T18:07:49+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "polymorfa/sdk": 20 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/examples/php/web-host/config/view.php b/examples/php/web-host/config/view.php new file mode 100644 index 00000000..fc430643 --- /dev/null +++ b/examples/php/web-host/config/view.php @@ -0,0 +1,3 @@ + [dirname(__DIR__).'/views'],'compiled' => (getenv('EXAMPLE_STATE_DIR') ?: '').'/framework/views']; diff --git a/examples/php/web-host/public/laravel.php b/examples/php/web-host/public/laravel.php new file mode 100644 index 00000000..5ef0131e --- /dev/null +++ b/examples/php/web-host/public/laravel.php @@ -0,0 +1,9 @@ +handleRequest(Illuminate\Http\Request::capture()); +} catch (Throwable) { + (new Symfony\Component\HttpFoundation\Response('Example application unavailable.', 503, ['Cache-Control' => 'no-store']))->send(); +} diff --git a/examples/php/web-host/public/router.php b/examples/php/web-host/public/router.php new file mode 100644 index 00000000..191ee862 --- /dev/null +++ b/examples/php/web-host/public/router.php @@ -0,0 +1,7 @@ + 'html']); + $host = new Host(fn (bool $authenticated, bool $partial, bool $localTest): string => $twig->render('workspace.html.twig', compact('authenticated', 'partial', 'localTest'))); + $request = Request::createFromGlobals(); + $request->attributes->set('_controller', fn (Request $request): Response => $host->handle($request)); + $kernel = new HttpKernel(new EventDispatcher(), new ControllerResolver(), argumentResolver:new ArgumentResolver()); + $response = $kernel->handle($request); + $response->send(); + $kernel->terminate($request, $response); +} catch (Throwable) { + (new Response('Example application unavailable.', 503, ['Cache-Control' => 'no-store']))->send(); +} diff --git a/examples/php/web-host/src/Conversation.php b/examples/php/web-host/src/Conversation.php new file mode 100644 index 00000000..977bd773 --- /dev/null +++ b/examples/php/web-host/src/Conversation.php @@ -0,0 +1,92 @@ +directory.'/conversation.lock', 'c+'); + try { + if ($lock === false || !flock($lock, LOCK_EX)) { throw new \RuntimeException('State unavailable.'); } + $path = $this->directory.'/conversation.json'; + $state = is_file($path) ? json_decode((string)file_get_contents($path), true, flags:JSON_THROW_ON_ERROR) : ['scope'=>hash('sha256',$this->session.':'.$this->peer),'messages'=>[],'attempts'=>[],'eventIds'=>[],'peerId'=>'','receivedEvents'=>0,'lastEvent'=>'']; + if (($state['scope']??null)!==hash('sha256',$this->session.':'.$this->peer)) { throw new \RuntimeException('Use a distinct state directory for this Number and recipient.'); } + $result = $operation($state); + if (file_put_contents($path,json_encode($state,JSON_THROW_ON_ERROR),LOCK_EX) === false) { throw new \RuntimeException('State unavailable.'); } + return $result; + } finally { + if (is_resource($lock)) { flock($lock,LOCK_UN);fclose($lock); } + umask($previous); + } + } + public function snapshot(): array + { + return $this->update(fn(array &$state): array => ['messages'=>$state['messages'],'peer'=>$this->peer,'session'=>$this->session,'receivedEvents'=>$state['receivedEvents'],'lastEvent'=>$state['lastEvent']]); + } + public function send(string $raw, string $key): array + { + try { $body = json_decode($raw,true,flags:JSON_THROW_ON_ERROR); } catch (\JsonException) { $body = null; } + if (strlen($raw)>16384 || !is_array($body) || array_keys($body)!==['text'] || !is_string($body['text']) || trim($body['text'])==='' || strlen($body['text'])>4096 || !preg_match('/^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$/D',$key)) { return [400,['error'=>'send_text_with_uuid_key']]; } + $previous=umask(0077);$lock=fopen($this->directory.'/send.lock','c+');umask($previous); + if ($lock===false) { throw new \RuntimeException('State unavailable.'); } + if (!flock($lock,LOCK_EX|LOCK_NB)) { fclose($lock);return [429,['error'=>'send_in_progress']]; } + try { + $prior=$this->update(function(array &$state) use($key,$body): ?array { + if (isset($state['attempts'][$key])) { + $attempt=$state['attempts'][$key]; + if ($attempt['text']!==$body['text']) { return [409,['error'=>'key_has_different_text']]; } + if ($attempt['message']!==null) { return [200,$attempt['message']]; } + } else { + if (count($state['attempts'])>=128) { return [429,['error'=>'example_send_capacity_reached']]; } + $state['attempts'][$key]=['text'=>$body['text'],'message'=>null]; + } + return null; + }); + if ($prior!==null) { return $prior; } + try { + $response=$this->client->messages->send($this->session,['conversation'=>['phoneNumber'=>$this->peer],'content'=>['text'=>$body['text']]],new RequestOptions(timeout:60,idempotencyKey:$key)); + $receipt=$response->data['data'];$timestamp=strtotime($receipt['timestamp']); + if (!$response->data['success'] || !$receipt['id'] || $timestamp===false) { throw new \RuntimeException('Invalid receipt.'); } + $message=['id'=>$receipt['id'],'clientId'=>$key,'text'=>$body['text'],'createdAt'=>$timestamp*1000,'direction'=>'outbound','status'=>'sent','receipt'=>$receipt['status']]; + $this->update(function(array &$state) use($key,$receipt,$message): void { + $state['peerId']=$receipt['conversation']['id']??$state['peerId']; + self::upsert($state,$message);$state['attempts'][$key]['message']=$message; + }); + return [200,$message]; + } catch (\Throwable) { return [502,['error'=>'send_outcome_unknown_retry_same_key']]; } + } finally { flock($lock,LOCK_UN);fclose($lock); } + } + private static function upsert(array &$state, array $message): void + { + foreach ($state['messages'] as $i=>$old) { if ($old['id']===$message['id']) { $state['messages'][$i]=$message;return; } } + $state['messages'][]=$message;$state['messages']=array_slice($state['messages'],-100); + } + public function receive(WebhookEvent $event): void + { + if ($event->session!==$this->session || !is_array($event->payload)) { return; } + $this->update(function(array &$state) use($event): void { + if (in_array($event->id,$state['eventIds'],true)) { return; } + $p=$event->payload;$c=$p['conversation']??[]; + $assigned=$state['peerId']!=='' ? ($c['id']??null)===$state['peerId'] : ltrim($c['phoneNumber']??'','+')===ltrim($this->peer,'+'); + if (!$assigned) { return; } + if ($event->event==='message.received') { + if (($p['fromMe']??null)!==false || ($p['isGroup']??null)!==false || !is_string($p['text']??null) || $p['text']==='' || strlen($p['text'])>4096 || !is_string($p['id']??null) || (!is_int($p['timestamp']??null) && !is_float($p['timestamp']??null))) { return; } + self::upsert($state,['id'=>$p['id'],'text'=>$p['text'],'createdAt'=>(int)($p['timestamp']*1000),'direction'=>'inbound','status'=>'sent','receipt'=>'received']); + } elseif ($event->event==='message.ack') { + $ranks=['sent'=>1,'server'=>1,'delivered'=>2,'delivery'=>2,'read'=>3,'played'=>3]; + $ids=array_column($p['messages']??[],'id');$receipt=$p['type']??''; + foreach ($state['messages'] as &$message) { if ($message['direction']==='outbound' && in_array($message['id'],$ids,true) && ($ranks[$receipt]??0)>($ranks[$message['receipt']]??0)) { $message['receipt']=$receipt; } }unset($message); + } else { return; } + $state['eventIds']=array_slice([...$state['eventIds'],$event->id],-1024);$state['receivedEvents']++;$state['lastEvent']=$event->event; + }); + } +} diff --git a/examples/php/web-host/src/Host.php b/examples/php/web-host/src/Host.php new file mode 100644 index 00000000..61bb809f --- /dev/null +++ b/examples/php/web-host/src/Host.php @@ -0,0 +1,169 @@ +username = self::setting('EXAMPLE_APP_USERNAME'); + $this->password = self::setting('EXAMPLE_APP_PASSWORD'); + $this->session = self::setting('POLYMORFA_SESSION'); + $this->origin = self::setting('EXAMPLE_APP_ORIGIN'); + if (!preg_match('~^http://127\.0\.0\.1:\d+$~D', $this->origin)) { + throw new \RuntimeException('Configure a loopback application origin.'); + } + $this->state = self::setting('EXAMPLE_STATE_DIR'); + if (!str_starts_with($this->state, '/') || str_starts_with($this->state, '/tmp/')) { + throw new \RuntimeException('Configure a persistent, private application state directory.'); + } + self::directory($this->state.'/sessions'); + $this->webhookSecret = self::setting('POLYMORFA_WEBHOOK_SECRET'); + $this->client = new MessagingClient(Credential::projectToken(self::setting('POLYMORFA_PROJECT_TOKEN')), getenv('POLYMORFA_API_BASE_URL') ?: 'https://api.polymorfa.com'); + $this->conversation = new Conversation($this->state,$this->session,self::setting('POLYMORFA_RECIPIENT'),$this->client); + } + + public static function directory(string $path): void + { + if (!is_dir($path) && !mkdir($path, 0700, true) && !is_dir($path)) { + throw new \RuntimeException('Application state is unavailable.'); + } + } + private static function setting(string $name): string + { + $value = getenv($name); + if ($value === false || $value === '') { + throw new \RuntimeException('Configure the application account, number and state.'); + } + return $value; + } + private function authenticated(Request $request): bool + { + $cookie = $request->cookies->get('polymorfa_example_session'); + if (!is_string($cookie) || !preg_match('/^[a-f0-9]{64}$/D', $cookie)) { + return false; + } + $path = $this->state.'/sessions/'.hash('sha256', $cookie); + $data = is_file($path) ? json_decode((string)file_get_contents($path), true) : null; + return is_array($data) && ($data['user'] ?? null) === 'local-app-user' && is_int($data['expires'] ?? null) && $data['expires'] > time(); + } + private function json(array $value, int $status = 200): Response + { + return new JsonResponse($value, $status, ['Cache-Control' => 'no-store, private','Vary' => 'Cookie']); + } + private function html(Request $request, bool $authenticated): Response + { + if ($request->headers->get('HX-Request') !== 'true' && $request->getPathInfo() !== '/') { + return new RedirectResponse('/'); + } + return new Response(($this->render)($authenticated, $request->headers->get('HX-Request') === 'true', getenv('POLYMORFA_EXAMPLE_TRANSPORT') === 'local-test-number'), 200, ['Cache-Control' => 'no-store, private']); + } + public function handle(Request $request): Response + { + $path = $request->getPathInfo(); + $methods = ['/' => 'GET','/api/login' => 'POST','/api/logout' => 'POST','/api/polymorfa/token' => 'POST','/api/polymorfa/connect' => 'POST','/api/polymorfa/conversation' => 'GET','/api/polymorfa/send' => 'POST','/webhooks/polymorfa' => 'POST']; + if (!isset($methods[$path])) { + return $this->json(['error' => 'not_found'], 404); + } + if ($request->getMethod() !== $methods[$path]) { + $response = $this->json(['error' => 'method_not_allowed'], 405); + $response->headers->set('Allow', $methods[$path]); + return $response; + } + if ($path === '/webhooks/polymorfa') { + $stream=$request->getContent(true);$raw=stream_get_contents($stream,65537); + if ($raw===false || strlen($raw)>65536) { return $this->json(['error'=>'body_too_large'],413); } + try { $this->conversation->receive(Webhooks::constructEvent($raw,$request->headers->get('x-webhook-signature',''),$this->webhookSecret)); } + catch (\Throwable) { return $this->json(['error'=>'invalid_webhook'],400); } + return $this->json(['received'=>true]); + } + if ($request->getMethod() === 'POST' && $request->headers->get('Origin') !== $this->origin) { + return $this->json(['error' => 'invalid_origin'], 403); + } + if ($path === '/') { + return $this->html($request, $this->authenticated($request)); + } + if ($path === '/api/login') { + if (strlen($request->getContent()) > 16384) { + return $this->json(['error' => 'invalid_sign_in'], 400); + } + $username = $request->request->get('username'); + $password = $request->request->get('password'); + if (!is_string($username) || !is_string($password) || !hash_equals(hash('sha256', $this->username), hash('sha256', $username)) || !hash_equals(hash('sha256', $this->password), hash('sha256', $password))) { + return $this->json(['error' => 'sign_in_failed'], 401); + } + $files = glob($this->state.'/sessions/*') ?: []; + foreach ($files as $file) { + if (filemtime($file) < time() - 1800) { + unlink($file); + } + } + if (count(glob($this->state.'/sessions/*') ?: []) >= 128) { + return $this->json(['error' => 'session_limit'], 429); + } + $cookie = bin2hex(random_bytes(32)); + $file = $this->state.'/sessions/'.hash('sha256', $cookie); + $previous = umask(0077); + try { + if (file_put_contents($file, json_encode(['user' => 'local-app-user','expires' => time() + 1800], JSON_THROW_ON_ERROR), LOCK_EX) === false) { + throw new \RuntimeException('Session storage failed.'); + } + } finally { + umask($previous); + } + $response = $this->html($request, true); + $response->headers->setCookie(Cookie::create('polymorfa_example_session', $cookie, time() + 1800, '/', httpOnly:true, sameSite:Cookie::SAMESITE_STRICT)); + return $response; + } + if ($path === '/api/logout') { + $cookie = $request->cookies->get('polymorfa_example_session'); + if (is_string($cookie) && preg_match('/^[a-f0-9]{64}$/D', $cookie)) { + $file = $this->state.'/sessions/'.hash('sha256', $cookie); + if (is_file($file)) { + unlink($file); + } + } + $response = $this->html($request, false); + $response->headers->clearCookie('polymorfa_example_session', '/', httpOnly:true, sameSite:Cookie::SAMESITE_STRICT); + return $response; + } + if (!$this->authenticated($request)) { + return $this->json(['error' => 'sign_in_required'], 401); + } + if ($path === '/api/polymorfa/conversation') { return $this->json($this->conversation->snapshot()); } + if ($path === '/api/polymorfa/send') { + $raw=stream_get_contents($request->getContent(true),16385); + [$status,$data]=$this->conversation->send($raw===false?'':$raw,$request->headers->get('Idempotency-Key','')); + return $this->json($data,$status); + } + try { + if ($path === '/api/polymorfa/connect') { + return $this->json($this->client->quickLinks->create(['purpose' => 'add_connection','session' => $this->session,'addConnection' => 'linked_devices','connectionGoal' => 'hybrid'])->data); + } + $result = $this->client->clientTokens->mint(['ephemeralId' => 'example:local-app-user','session' => $this->session,'ttlSeconds' => 600]); + $token = $result->data['data']; + $expires = strtotime($token['expiresAt']); + if ($expires === false || $expires <= time() || !str_starts_with($token['token'], 'pmfa_ct_')) { + return $this->json(['error' => 'invalid_browser_grant'], 502); + } + return $this->json(['value' => $token['token'],'audience' => 'browser','expiresAt' => $expires * 1000,'scopes' => ['read_messages','connect_whatsapp'],'grant' => ['session' => $this->session,'conversations' => [],'allow' => ['read_messages','connect_whatsapp']]]); + } catch (\Throwable) { + return $this->json(['error' => 'browser_access_unavailable'],502); + } + } +} diff --git a/examples/php/web-host/tests/run.php b/examples/php/web-host/tests/run.php new file mode 100644 index 00000000..ed134598 --- /dev/null +++ b/examples/php/web-host/tests/run.php @@ -0,0 +1,107 @@ + 'example-user','EXAMPLE_APP_PASSWORD' => 'test-only-password','POLYMORFA_PROJECT_TOKEN' => 'pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k','POLYMORFA_SESSION' => 'authorized-support','POLYMORFA_RECIPIENT' => '+15550001234','POLYMORFA_WEBHOOK_SECRET' => 'local-test-signing-key']); + $env['EXAMPLE_STATE_DIR'] = $state.'/'.$framework; + $env['EXAMPLE_FRAMEWORK'] = $framework; + $env['EXAMPLE_APP_ORIGIN'] = 'http://127.0.0.1:31306'; + $env['POLYMORFA_API_BASE_URL'] = 'http://127.0.0.1:31305'; + if (!is_dir($env['EXAMPLE_STATE_DIR'])) { + mkdir($env['EXAMPLE_STATE_DIR'], 0700, true); + } + $counter = $env['EXAMPLE_STATE_DIR'].'/native-mints'; + @unlink($env['EXAMPLE_STATE_DIR'].'/conversation.json'); + @unlink($env['EXAMPLE_STATE_DIR'].'/native-sends'); + $linkCounter = $env['EXAMPLE_STATE_DIR'].'/native-links'; + if (is_file($linkCounter)) { unlink($linkCounter); } + if (is_file($counter)) { + unlink($counter); + } + $log = fopen($env['EXAMPLE_STATE_DIR'].'/server.log', 'ab'); + $servers = [ + proc_open([PHP_BINARY,'-S','127.0.0.1:31305',$root.'/tests/upstream.php'], [STDIN,$log,$log], $pipes, $root, $env), + proc_open([PHP_BINARY,'-S','127.0.0.1:31306','-t',$root.'/public',$root.'/public/router.php'], [STDIN,$log,$log], $pipes, $root, $env), + ]; + try { + $client = new Client(['base_uri' => $env['EXAMPLE_APP_ORIGIN'],'cookies' => new CookieJar(),'http_errors' => false,'allow_redirects' => false,'timeout' => 10]); + $ready = false; + for ($attempt = 0;$attempt < 100;$attempt++) { + try { + $response = $client->get('/'); + $ready = true; + break; + } catch (Throwable) { + usleep(50000); + } + } + check($ready, 'server starts'); + check($response->getStatusCode() === 200 && str_contains((string)$response->getBody(), 'type="password"'), $framework.' initial template status '.$response->getStatusCode()); + $send = fn (string $method, string $path, string $origin, array $fields = []) => $client->request($method, $path, ['headers' => ['Origin' => $origin,'HX-Request' => 'true'],'form_params' => $fields]); + $origin = $env['EXAMPLE_APP_ORIGIN']; + check($send('POST', '/api/polymorfa/token', $origin)->getStatusCode() === 401, 'anonymous'); + check($send('POST', '/api/polymorfa/connect', $origin)->getStatusCode() === 401, 'anonymous connect'); + check($send('GET', '/api/login', $origin)->getStatusCode() === 405, 'GET login'); + check($send('POST', '/api/login', 'https://foreign.invalid', ['username' => 'example-user','password' => 'test-only-password'])->getStatusCode() === 403, 'foreign origin'); + check($send('POST', '/api/login', $origin, ['username' => 'example-user','password' => 'wrong'])->getStatusCode() === 401, 'invalid password'); + $response = $send('POST', '/api/login', $origin, ['username' => 'example-user','password' => 'test-only-password']); + check($response->getStatusCode() === 200 && str_contains((string)$response->getBody(), 'pmfa-inbox'), 'authenticated template'); + check(str_contains($response->getHeaderLine('Set-Cookie'), 'httponly') && str_contains($response->getHeaderLine('Set-Cookie'), 'samesite=strict'), 'opaque private cookie'); + $response = $send('POST', '/api/polymorfa/token', $origin, ['session' => 'attacker','ephemeralId' => 'attacker']); + $token = json_decode((string)$response->getBody(), true); + check($response->getStatusCode() === 200 && ($token['value'] ?? null) === 'pmfa_ct_YnJvd3Nlcl9maXh0dXJl' && ($token['audience'] ?? null) === 'browser' && ($token['expiresAt'] ?? 0) > time() * 1000 && str_contains($response->getHeaderLine('Cache-Control'), 'no-store'), 'native SDK token mint'); + check((int)file_get_contents($counter) === 1, 'one real upstream request'); + $response = $send('POST', '/api/polymorfa/connect', $origin, ['session' => 'attacker','purpose' => 'initial']); + $raw = (string)$response->getBody(); $link = json_decode($raw, true); + check($response->getStatusCode() === 200 && ($link['data']['url'] ?? null) === 'https://quick.example.test/link' && !str_contains($raw, 'pmfa_pt_'), 'assigned supplementary QuickLink'); + check((int)file_get_contents($linkCounter) === 1, 'one QuickLink upstream request'); + check($send('POST', '/api/polymorfa/connect', 'https://foreign.invalid')->getStatusCode() === 403, 'QuickLink origin guard'); + check($send('POST', '/api/polymorfa/token', 'https://foreign.invalid')->getStatusCode() === 403, 'mint origin guard'); + $key = 'ad87d91a-32fc-48ab-aa00-5973d1e9f98d'; + $message = fn(array $body) => $client->post('/api/polymorfa/send', ['headers'=>['Origin'=>$origin,'Idempotency-Key'=>$key],'json'=>$body]); + check($message(['text'=>'Native send','recipient'=>'foreign'])->getStatusCode() === 400, 'fixed target'); + $sent = $message(['text'=>'Native send']); + check($sent->getStatusCode() === 200 && json_decode((string)$sent->getBody(),true)['id'] === 'message-out', 'native send'); + check($message(['text'=>'Native send'])->getStatusCode() === 200, 'same key replay'); + check($message(['text'=>'Changed'])->getStatusCode() === 409, 'changed text rejected'); + check((int)file_get_contents($env['EXAMPLE_STATE_DIR'].'/native-sends') === 1, 'no duplicate upstream send'); + $envelope = ['id'=>'event-1','timestamp'=>'2026-10-18T10:00:01Z','session'=>'authorized-support','event'=>'message.received','payload'=>[ + 'id'=>'message-in','conversation'=>['id'=>'canonical-peer'],'fromMe'=>false,'isGroup'=>false,'timestamp'=>1792317601,'text'=>'Independent reply','type'=>'text','pushName'=>'Test peer','whatsapp_ids'=>['linked_devices'=>'wire-in'] + ]]; + $body = json_encode($envelope,JSON_THROW_ON_ERROR); + $webhook = fn(string $signature) => $client->post('/webhooks/polymorfa',['headers'=>['x-webhook-signature'=>$signature],'body'=>$body]); + check($webhook('bad')->getStatusCode() === 400, 'invalid webhook signature'); + for ($i=0;$i<2;$i++) { check($webhook(hash_hmac('sha256',$body,'local-test-signing-key'))->getStatusCode() === 200, 'signed receiver'); } + $snapshot = json_decode((string)$client->get('/api/polymorfa/conversation')->getBody(),true); + check(array_column($snapshot['messages'],'text') === ['Native send','Independent reply'] && $snapshot['receivedEvents'] === 1, 'signed reply deduplicated'); + $client->get('/'); + check(json_decode((string)$client->get('/api/polymorfa/conversation')->getBody(),true) === $snapshot, 'reload preserves conversation'); + check($send('POST', '/api/logout', $origin)->getStatusCode() === 200, 'logout'); + check($client->get('/api/polymorfa/conversation')->getStatusCode() === 401 && $message(['text'=>'Native send'])->getStatusCode() === 401,'logout revokes conversation and send'); + check($send('POST', '/api/polymorfa/token', $origin)->getStatusCode() === 401, 'revoked application session'); + check((int)file_get_contents($counter) === 1, 'no unauthorized upstream requests'); + check($send('POST', '/api/polymorfa/connect', $origin)->getStatusCode() === 401 && (int)file_get_contents($linkCounter) === 1, 'revoked QuickLink access'); + echo $framework." real HTTP/auth/token boundary passed\n"; + } finally { + foreach ($servers as $server) { + if (is_resource($server)) { + proc_terminate($server); + proc_close($server); + } + } + fclose($log); + } +} diff --git a/examples/php/web-host/tests/serve.php b/examples/php/web-host/tests/serve.php new file mode 100644 index 00000000..4eca71a0 --- /dev/null +++ b/examples/php/web-host/tests/serve.php @@ -0,0 +1,24 @@ + is_file($state) ? (int)file_get_contents($state) : 0]); + return; +} +$body = json_decode(file_get_contents('php://input'), true); +if ($_SERVER['REQUEST_URI'] === '/messaging/authorized-support/messages/send') { + if ($_SERVER['REQUEST_METHOD'] !== 'POST' || ($_SERVER['HTTP_AUTHORIZATION'] ?? '') !== 'Bearer pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k' || ($_SERVER['HTTP_IDEMPOTENCY_KEY'] ?? '') !== 'ad87d91a-32fc-48ab-aa00-5973d1e9f98d' || $body !== ['conversation'=>['phoneNumber'=>'+15550001234'],'content'=>['text'=>'Native send']]) { http_response_code(422); echo '{"error":"fixture_mismatch"}'; return; } + $counter = getenv('EXAMPLE_STATE_DIR').'/native-sends'; + file_put_contents($counter,(string)((is_file($counter)?(int)file_get_contents($counter):0)+1)); + echo json_encode(['success'=>true,'data'=>['id'=>'message-out','timestamp'=>'2026-10-18T10:00:00.750Z','status'=>'sent','conversation'=>['id'=>'canonical-peer'],'whatsapp_ids'=>['linked_devices'=>'wire-out']]]);return; +} + +if ($_SERVER['REQUEST_URI'] === '/messaging/quicklinks') { + if ($_SERVER['REQUEST_METHOD'] !== 'POST' || ($_SERVER['HTTP_AUTHORIZATION'] ?? '') !== 'Bearer pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k' || ($_SERVER['HTTP_POLYMORFA_VERSION'] ?? '') !== '2026-09-22' || $body !== ['purpose' => 'add_connection','session' => 'authorized-support','addConnection' => 'linked_devices','connectionGoal' => 'hybrid']) { http_response_code(422); echo '{"error":"fixture_mismatch"}'; return; } + $links = getenv('EXAMPLE_STATE_DIR').'/native-links'; + file_put_contents($links, (string)((is_file($links) ? (int)file_get_contents($links) : 0) + 1), LOCK_EX); + echo json_encode(['success' => true,'data' => ['purpose' => 'add_connection','session' => 'authorized-support','addConnection' => 'linked_devices','connectionGoal' => 'hybrid','id' => 'link','url' => 'https://quick.example.test/link','expiresAt' => null]]); return; +} + +if ($_SERVER['REQUEST_METHOD'] !== 'POST' || $_SERVER['REQUEST_URI'] !== '/platform/client-tokens' || ($_SERVER['HTTP_AUTHORIZATION'] ?? '') !== 'Bearer pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k' || ($_SERVER['HTTP_POLYMORFA_VERSION'] ?? '') !== '2026-09-22' || $body !== ['ephemeralId' => 'example:local-app-user','session' => 'authorized-support','ttlSeconds' => 600]) { + http_response_code(422); + echo '{"error":"fixture_mismatch"}'; + return; +} +file_put_contents($state, (string)((is_file($state) ? (int)file_get_contents($state) : 0) + 1), LOCK_EX); +echo json_encode(['success' => true,'data' => ['token' => 'pmfa_ct_YnJvd3Nlcl9maXh0dXJl','expiresAt' => gmdate('c', time() + 600)]]); diff --git a/examples/php/web-host/views/workspace.blade.php b/examples/php/web-host/views/workspace.blade.php new file mode 100644 index 00000000..50c68bef --- /dev/null +++ b/examples/php/web-host/views/workspace.blade.php @@ -0,0 +1,13 @@ +@if (!$partial) +PHP Polymorfa workspace +@endif +

Messaging workspace

+@if ($authenticated) +@if($localTest)

Local test numbers. Simulated WhatsApp transport.

@endif

Text messages use the server-assigned number and recipient. Conversation state is kept in this example's private state directory.

Waiting for the native SDK conversation.

+@else +

Sign in with the local example application's configured account.

+@endif +
+@if (!$partial) + +@endif diff --git a/examples/php/web-host/views/workspace.html.twig b/examples/php/web-host/views/workspace.html.twig new file mode 100644 index 00000000..f3e25434 --- /dev/null +++ b/examples/php/web-host/views/workspace.html.twig @@ -0,0 +1,13 @@ +{% if not partial %} +PHP Polymorfa workspace +{% endif %} +

Messaging workspace

+{% if authenticated %} +{% if localTest %}

Local test numbers. Simulated WhatsApp transport.

{% endif %}

Text messages use the server-assigned number and recipient. Conversation state is kept in this example's private state directory.

Waiting for the native SDK conversation.

+{% else %} +

Sign in with the local example application's configured account.

+{% endif %} +
+{% if not partial %} + +{% endif %} diff --git a/examples/python/README.md b/examples/python/README.md new file mode 100644 index 00000000..0cd7797f --- /dev/null +++ b/examples/python/README.md @@ -0,0 +1,13 @@ +# Python server examples + +These examples use the unpublished source SDK. From the repository root, run `uv run --project packages/python examples/python/send.py`. Set `POLYMORFA_API_KEY`, `POLYMORFA_SESSION`, `POLYMORFA_RECIPIENT`, and `POLYMORFA_IDEMPOTENCY_KEY` in your server environment. The send example makes a real authorized API request. + +For FastAPI, install the `fastapi` extra and run the webhook app with an ASGI server; include the Django view in your existing application's URL configuration with the `django` extra. Set `POLYMORFA_WEBHOOK_SECRET`. Both adapters verify original request bytes, retain unknown events, and reject altered payloads. Add durable deduplication and processing in your application. + +Install the `calls` extra for `calls_media.py`. The example attaches to an already admitted Call using server credentials and an authorized participant. Set the Call ID, connection ID and participant environment variables named in the source. Calls access, connection permissions and service enrollment are enforced by the API. This example receives PCM; it does not capture a microphone. + +`programmatic_calls.py` answers incoming Calls and consumes negotiated PCM with +the high-level server client. `web_host/` provides runnable FastAPI and Django +application hosts with templates, HTMX, authenticated token minting and the +existing TypeScript Web Components. See its README for environment variables +and run commands. diff --git a/examples/python/calls_media.py b/examples/python/calls_media.py new file mode 100644 index 00000000..3fc33c54 --- /dev/null +++ b/examples/python/calls_media.py @@ -0,0 +1,24 @@ +"""Attach to an already admitted server Call; no microphone or browser dependency.""" + +import asyncio +import os + +from polymorfa import Credential +from polymorfa.calls import AudioFrame, MediaSocket + + +async def main() -> None: + async with MediaSocket( + Credential("organization_api_key", os.environ["POLYMORFA_API_KEY"]), + os.environ["POLYMORFA_CALL_ID"], + os.environ["POLYMORFA_CONNECTION_ID"], + participant=os.environ["POLYMORFA_PARTICIPANT"], + ) as socket: + frame = await socket.receive() + if isinstance(frame, AudioFrame): + print(f"Received {len(frame.samples)} samples at {socket.sample_rate} Hz") + await socket.leave() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/python/django_webhook.py b/examples/python/django_webhook.py new file mode 100644 index 00000000..4759b89e --- /dev/null +++ b/examples/python/django_webhook.py @@ -0,0 +1,21 @@ +"""Include this view in your Django URL configuration.""" + +import os + +from django.http import HttpRequest, JsonResponse +from django.views.decorators.csrf import csrf_exempt +from polymorfa import WebhookSignatureError, is_event +from polymorfa.integrations import django_webhook_event + + +@csrf_exempt +def webhook(request: HttpRequest) -> JsonResponse: + if request.method != "POST": + return JsonResponse({"error": "method_not_allowed"}, status=405) + try: + event = django_webhook_event(request, os.environ["POLYMORFA_WEBHOOK_SECRET"]) + except WebhookSignatureError: + return JsonResponse({"error": "invalid_signature"}, status=400) + if is_event(event, "message.received"): + return JsonResponse({"received": event.id, "message": event.payload["id"]}) + return JsonResponse({"received": event.id}) diff --git a/examples/python/fastapi_webhook.py b/examples/python/fastapi_webhook.py new file mode 100644 index 00000000..1f21a49f --- /dev/null +++ b/examples/python/fastapi_webhook.py @@ -0,0 +1,25 @@ +"""FastAPI endpoint authenticating original bytes before application JSON parsing.""" + +import os + +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from polymorfa import WebhookSignatureError, is_event +from polymorfa.integrations import fastapi_webhook_event + +app = FastAPI() + + +@app.post("/webhooks/polymorfa") +async def webhook(request: Request) -> JSONResponse: + try: + event = await fastapi_webhook_event( + request, os.environ["POLYMORFA_WEBHOOK_SECRET"] + ) + except WebhookSignatureError: + return JSONResponse({"error": "invalid_signature"}, status_code=400) + # Store event.id in your delivery deduplication table before processing. + # Unknown event payloads remain available in event.payload. + if is_event(event, "message.received"): + return JSONResponse({"received": event.id, "message": event.payload["id"]}) + return JSONResponse({"received": event.id}) diff --git a/examples/python/programmatic_calls.py b/examples/python/programmatic_calls.py new file mode 100644 index 00000000..dc53a93f --- /dev/null +++ b/examples/python/programmatic_calls.py @@ -0,0 +1,27 @@ +"""Install polymorfa-sdk[calls]; receive programmatic PCM without recording it.""" + +import asyncio +import os + +from polymorfa import CallsClient + + +async def main() -> None: + async with CallsClient( + os.environ["POLYMORFA_API_KEY"], + os.environ["POLYMORFA_SESSION"], + participant="voice-worker", + ) as client: + async for call in client.incoming_calls(): + await call.answer(exclusive=True) + print("Connected", call.id, "PCM rate", call.sample_rate) + try: + async for frame in call.audio.frames(): + # Feed frame.samples to your transcriber. No audio is retained here. + print("Received samples", len(frame.samples)) + finally: + await call.leave() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/python/send.py b/examples/python/send.py new file mode 100644 index 00000000..d974c7d4 --- /dev/null +++ b/examples/python/send.py @@ -0,0 +1,27 @@ +"""Run with POLYMORFA_API_KEY and POLYMORFA_SESSION from your server environment.""" + +import asyncio +import os + +from polymorfa import AsyncMessagingClient, Credential, RequestOptions + + +async def main() -> None: + async with AsyncMessagingClient( + Credential("organization_api_key", os.environ["POLYMORFA_API_KEY"]) + ) as client: + response = await client.messages.send( + os.environ["POLYMORFA_SESSION"], + { + "conversation": {"phoneNumber": os.environ["POLYMORFA_RECIPIENT"]}, + "content": {"text": "Hello from Python"}, + }, + options=RequestOptions( + idempotency_key=os.environ["POLYMORFA_IDEMPOTENCY_KEY"] + ), + ) + print(response.data["data"]["id"], response.metadata.request_id) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/python/web_host/.gitignore b/examples/python/web_host/.gitignore new file mode 100644 index 00000000..ee889666 --- /dev/null +++ b/examples/python/web_host/.gitignore @@ -0,0 +1 @@ +assets/ diff --git a/examples/python/web_host/README.md b/examples/python/web_host/README.md new file mode 100644 index 00000000..2759278f --- /dev/null +++ b/examples/python/web_host/README.md @@ -0,0 +1,67 @@ +# Python web hosts + +FastAPI and Django run the same loopback application. Jinja templates render +the page, HTMX submits sign-in and sign-out, and the browser mounts the existing +Polymorfa Web Components. The Python SDK remains in the server process. + +From the repository root: + +```sh +npm ci +node examples/python/web_host/build-assets.mjs +uv run --project packages/python --extra fastapi uvicorn \ + --app-dir examples/python/web_host fastapi_app:app --host 127.0.0.1 --port 8080 +``` + +Set `EXAMPLE_APP_USERNAME`, `EXAMPLE_APP_PASSWORD`, `POLYMORFA_PROJECT_TOKEN` and +`POLYMORFA_SESSION`, `POLYMORFA_RECIPIENT` and `POLYMORFA_WEBHOOK_SECRET` in the server environment before starting the host. Open +`http://127.0.0.1:8080`. Use the `django` extra and `django_app:application` to +run Django. `EXAMPLE_APP_ORIGIN` changes the exact allowed loopback origin; +`POLYMORFA_API_BASE_URL` optionally changes the API origin. Asset building +bundles the workspace Web Components and checks the SHA-256 digest of HTMX 2.0.4. + +The example has one configured application account. Sign-in creates a random +server-held session with a 30-minute lifetime and an HttpOnly, SameSite=Strict +cookie. POST routes check the application origin. The token endpoint derives +the application identity and authorized number on the server and requests a +ten-minute client token. Browser input cannot select a number or identity. +The endpoint returns only browser claims with `Cache-Control: no-store`. +Sign-out revokes the application session and disposes the browser client. +The connection indicator describes the browser grant; it does not establish +a connected WhatsApp number. + +The inbox reads an authenticated, application-owned in-memory conversation. Text +sends call the native Python SDK with a caller UUID idempotency key and the +server-assigned Number and recipient. Unknown outcomes retain that key for a +retry; a changed body with the same key is rejected. The example keeps at most +128 send attempts and 100 messages until the process stops. It does not enable HMS. + +Register `/webhooks/polymorfa` as a native webhook for the assigned Number with +`message.received` and `message.ack`, using the same signing secret. Both frameworks +verify the original bounded request bytes through the Python SDK before updating +the inbox. Display timestamps use whole seconds, matching native message events, so a +same-second reply retains arrival order rather than sorting before its send. +Incoming events must match the Number and canonical conversation ID +from the send receipt. Phone matching is only a fallback before that ID is known. +Duplicate event IDs are ignored within a bounded 1024-event window. +`POLYMORFA_EXAMPLE_TRANSPORT=local-test-number` labels simulated transport in the UI. + +Before public hosting, integrate live application membership, authorization, +HTTPS, durable sessions and durable conversation/deduplication storage. Keep +project and organization credentials in the server. The separate token and +supplementary connection routes remain examples; this inbox does not use them. + +`packages/python/tests/test_browser_hosts.py` runs both ASGI applications over +real HTTP. It verifies the upstream credential and API version, server-selected +mint fields, POST-only sign-in, invalid credentials, foreign origins and +revocation after sign-out. Both hosts were also exercised in a browser with a +synthetic upstream: sign-in mounted the real components, one mint served the +browser session, the inbox rendered, and sign-out removed the components. +No real number, customer credentials or live provider was used. + +The Connect number endpoint creates a hosted supplementary linked-device invitation for the server-assigned `POLYMORFA_SESSION`. Assign an eligible Official Number that can add linked devices. The API still checks assignment, eligibility, consent and funding. Browser fields cannot choose another Number or change custody. This does not create an initial Number or establish live WhatsApp pairing. + +The host HTTP tests also verify native sends, fixed targets, idempotent replay, +signed replies, event deduplication, reload and logout for both frameworks. +Local native Inspector video acceptance is recorded separately from these HTTP +tests and does not establish staging, production or registry availability. diff --git a/examples/python/web_host/bridge.js b/examples/python/web_host/bridge.js new file mode 100644 index 00000000..bb76c284 --- /dev/null +++ b/examples/python/web_host/bridge.js @@ -0,0 +1,150 @@ +import { definePolymorfaElements } from "@polymorfa/elements"; +definePolymorfaElements(); +let disposeInbox; +// Only receipts the host observed from message.ack become chat receipts. The +// send response status is upstream acceptance, never delivery or read. +const observedReceipts = new Map([ + ["delivered", "delivered"], + ["delivery", "delivered"], + ["read", "read"], + ["played", "played"], +]); +function chatMessage({ receipt, ...message }) { + const state = + message.direction === "outbound" + ? observedReceipts.get(receipt) + : undefined; + return state ? { ...message, receipt: { state } } : message; +} +// The browser renders shared components. Every send and incoming message is +// handled by this application's native Python SDK, not the browser API transport. +function nativeInbox(inbox) { + const changes = new Set(); + const messages = new Set(); + let previous = ""; + let stopped = false; + let timer; + const request = async (path, options = {}) => { + const response = await fetch(`/api/polymorfa/${path}`, { + credentials: "same-origin", + ...options, + }); + if (!response.ok) throw new Error(await response.text()); + return response.json(); + }; + const row = (snapshot) => { + const last = snapshot.messages.at(-1); + return { + id: snapshot.peer, + session: snapshot.session, + name: "Assigned recipient", + phoneNumber: snapshot.peer, + lastActivity: last?.createdAt ?? 0, + unreadCount: 0, + ...(last + ? { + lastMessage: { + text: last.text, + createdAt: last.createdAt, + direction: last.direction, + }, + } + : {}), + }; + }; + const poll = async () => { + try { + const snapshot = await request("conversation"); + if (stopped) return; + const serialized = JSON.stringify(snapshot); + if (serialized !== previous) { + previous = serialized; + for (const notify of changes) + notify({ type: "conversation", conversation: row(snapshot) }); + for (const message of snapshot.messages) + for (const notify of messages) + notify({ type: "upsert", message: chatMessage(message) }); + } + const status = document.querySelector("#native-status"); + if (status) { + const outgoing = snapshot.messages.findLast( + (m) => m.direction === "outbound", + ); + status.textContent = + `Python SDK · ${snapshot.receivedEvents} verified webhook events` + + (outgoing ? ` · Latest send: ${outgoing.receipt}` : ""); + } + } catch (error) { + const status = document.querySelector("#native-status"); + if (status && !stopped) status.textContent = error.message; + } finally { + if (!stopped) timer = setTimeout(poll, 800); + } + }; + inbox.source = { + async listConversations(_cursor, signal) { + return { + conversations: [row(await request("conversation", { signal }))], + }; + }, + subscribe(notify) { + changes.add(notify); + return () => changes.delete(notify); + }, + conversation() { + return { + async load(_cursor, signal) { + return { + messages: (await request("conversation", { signal })).messages.map( + chatMessage, + ), + }; + }, + subscribe(notify) { + messages.add(notify); + return () => messages.delete(notify); + }, + async send(message, signal) { + if (message.replyTo || message.attachments?.length) + throw new Error("This native example sends text only."); + return chatMessage( + await request("send", { + method: "POST", + signal, + headers: { + "Content-Type": "application/json", + "Idempotency-Key": message.clientId, + }, + body: JSON.stringify({ text: message.text }), + }), + ); + }, + }; + }, + }; + void poll(); + return () => { + stopped = true; + clearTimeout(timer); + changes.clear(); + messages.clear(); + }; +} +function mount() { + const inbox = document.querySelector("pmfa-inbox"); + if (inbox) { + disposeInbox ??= nativeInbox(inbox); + } else { + disposeInbox?.(); + disposeInbox = undefined; + } +} +document.addEventListener("htmx:afterSwap", mount); +document.addEventListener("htmx:responseError", () => { + const error = document.querySelector("#error"); + if (error) + error.textContent = "The request failed. Check your sign-in and try again."; +}); +if (document.readyState === "loading") + document.addEventListener("DOMContentLoaded", mount, { once: true }); +else mount(); diff --git a/examples/python/web_host/build-assets.mjs b/examples/python/web_host/build-assets.mjs new file mode 100644 index 00000000..a6a2c4af --- /dev/null +++ b/examples/python/web_host/build-assets.mjs @@ -0,0 +1,24 @@ +import { build } from "esbuild"; +import { createHash } from "node:crypto"; +import { writeFile, mkdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +await mkdir(new URL("assets/", import.meta.url), { recursive: true }); +await build({ + entryPoints: [fileURLToPath(new URL("bridge.js", import.meta.url))], + outfile: fileURLToPath(new URL("assets/elements.js", import.meta.url)), + bundle: true, + format: "esm", + platform: "browser", + target: "es2022", +}); +const response = await fetch( + "https://raw.githubusercontent.com/bigskysoftware/htmx/v2.0.4/dist/htmx.min.js", +); +if (!response.ok) throw new Error("Cannot fetch pinned HTMX asset."); +const body = Buffer.from(await response.arrayBuffer()); +if ( + createHash("sha256").update(body).digest("hex") !== + "e209dda5c8235479f3166defc7750e1dbcd5a5c1808b7792fc2e6733768fb447" +) + throw new Error("Pinned HTMX integrity check failed."); +await writeFile(new URL("assets/htmx.js", import.meta.url), body); diff --git a/examples/python/web_host/django_app.py b/examples/python/web_host/django_app.py new file mode 100644 index 00000000..2700bc6e --- /dev/null +++ b/examples/python/web_host/django_app.py @@ -0,0 +1,118 @@ +"""uvicorn django_app:application --host 127.0.0.1 --port 8080""" + +from django.conf import settings + +if not settings.configured: + settings.configure( + DEBUG=False, + SECRET_KEY="loopback-example-no-django-sessions", + ROOT_URLCONF=__name__, + DATA_UPLOAD_MAX_MEMORY_SIZE=65536, + ALLOWED_HOSTS=["127.0.0.1", "localhost", "example.test"], + ) + +from django.core.asgi import get_asgi_application +from django.http import HttpResponse, HttpResponseRedirect, JsonResponse +from django.urls import path, re_path +from django.views.decorators.csrf import csrf_exempt +from django.views.static import serve +from host import COOKIE, ROOT, Host + +from polymorfa import PolymorfaError +from polymorfa.integrations import django_webhook_event + +settings.ROOT_URLCONF = __name__ +settings.ALLOWED_HOSTS = ["127.0.0.1", "localhost", "example.test"] + +host = Host() + + +def workspace(request, authenticated): + response = ( + HttpResponse(host.render(authenticated, fragment=True)) + if request.headers.get("hx-request") == "true" + else HttpResponseRedirect("/") + ) + response["cache-control"] = "no-store" + if response.status_code == 302: + response.status_code = 303 + return response + + +@csrf_exempt +async def view(request, operation="index"): + if operation == "index" and request.method == "GET": + response = HttpResponse(host.render(host.authenticated(request.COOKIES.get(COOKIE)))) + response["cache-control"] = "no-store" + return response + if operation == "conversation" and request.method == "GET": + if not host.authenticated(request.COOKIES.get(COOKIE)): + return JsonResponse({"error": "sign_in_required"}, status=401) + response = JsonResponse(host.snapshot()) + response["cache-control"] = "no-store" + return response + if request.method != "POST": + return JsonResponse({"error": "method_not_allowed"}, status=405) + if operation == "webhook": + try: + if len(request.body) > 65536: + return JsonResponse({"error": "body_too_large"}, status=413) + host.receive(django_webhook_event(request, host.webhook_secret)) + except (PolymorfaError, ValueError, TypeError, KeyError, OverflowError): + return JsonResponse({"error": "invalid_webhook"}, status=400) + return JsonResponse({"received": True}) + if request.headers.get("origin") != host.origin: + return JsonResponse({"error": "invalid_origin"}, status=403) + if operation == "login": + reply = host.login(request.body) + if reply.session is None: + return JsonResponse(reply.data, status=reply.status) + response = workspace(request, True) + response.set_cookie( + COOKIE, + reply.session, + max_age=1800, + httponly=True, + samesite="Strict", + secure=host.origin.startswith("https:"), + ) + return response + if operation == "logout": + host.logout(request.COOKIES.get(COOKIE)) + response = workspace(request, False) + response.delete_cookie(COOKIE, samesite="Strict") + return response + if not host.authenticated(request.COOKIES.get(COOKIE)): + return JsonResponse({"error": "sign_in_required"}, status=401) + if operation == "send": + reply = await host.send_message(request.body, request.headers.get("idempotency-key", "")) + response = JsonResponse(reply.data, status=reply.status) + response["cache-control"] = "no-store" + return response + try: + result = ( + await host.mint() + if operation == "token" + else await host.connect_number() + if operation == "connect" + else None + ) + except (PolymorfaError, ValueError, TypeError, KeyError, OverflowError): + return JsonResponse({"error": "browser_access_unavailable"}, status=502) + response = JsonResponse(result, safe=False, status=404 if result is None else 200) + response["cache-control"] = "no-store" + return response + + +urlpatterns = [ + path("", view), + path("api/login", view, {"operation": "login"}), + path("api/logout", view, {"operation": "logout"}), + path("api/polymorfa/token", view, {"operation": "token"}), + path("api/polymorfa/connect", view, {"operation": "connect"}), + path("api/polymorfa/conversation", view, {"operation": "conversation"}), + path("api/polymorfa/send", view, {"operation": "send"}), + path("webhooks/polymorfa", view, {"operation": "webhook"}), + re_path(r"^assets/(?P.*)$", serve, {"document_root": ROOT / "assets"}), +] +application = get_asgi_application() diff --git a/examples/python/web_host/fastapi_app.py b/examples/python/web_host/fastapi_app.py new file mode 100644 index 00000000..854c612e --- /dev/null +++ b/examples/python/web_host/fastapi_app.py @@ -0,0 +1,108 @@ +"""uvicorn fastapi_app:app --host 127.0.0.1 --port 8080""" + +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from host import COOKIE, ROOT, Host + +from polymorfa import PolymorfaError +from polymorfa.integrations import fastapi_webhook_event + +app, host = FastAPI(), Host() +app.mount("/assets", StaticFiles(directory=ROOT / "assets", check_dir=False), name="assets") + + +def workspace(request: Request, authenticated: bool): + if request.headers.get("hx-request") == "true": + return HTMLResponse( + host.render(authenticated, fragment=True), + headers={"cache-control": "no-store"}, + ) + return RedirectResponse("/", status_code=303, headers={"cache-control": "no-store"}) + + +@app.get("/") +async def index(request: Request): + return HTMLResponse( + host.render(host.authenticated(request.cookies.get(COOKIE))), + headers={"cache-control": "no-store"}, + ) + + +@app.post("/api/login") +async def login(request: Request): + if request.headers.get("origin") != host.origin: + return JSONResponse({"error": "invalid_origin"}, status_code=403) + reply = host.login(await request.body()) + if reply.session is None: + return JSONResponse(reply.data, status_code=reply.status) + response = workspace(request, True) + response.set_cookie( + COOKIE, + reply.session, + max_age=1800, + httponly=True, + samesite="strict", + secure=host.origin.startswith("https:"), + ) + return response + + +@app.post("/api/logout") +async def logout(request: Request): + if request.headers.get("origin") != host.origin: + return JSONResponse({"error": "invalid_origin"}, status_code=403) + host.logout(request.cookies.get(COOKIE)) + response = workspace(request, False) + response.delete_cookie(COOKIE, httponly=True, samesite="strict") + return response + + +@app.post("/api/polymorfa/{operation}") +async def browser_action(operation: str, request: Request): + if request.headers.get("origin") != host.origin: + return JSONResponse({"error": "invalid_origin"}, status_code=403) + if not host.authenticated(request.cookies.get(COOKIE)): + return JSONResponse({"error": "sign_in_required"}, status_code=401) + if operation == "send": + body = bytearray() + async for chunk in request.stream(): + if len(body) + len(chunk) > 16384: + return JSONResponse({"error": "body_too_large"}, status_code=413) + body.extend(chunk) + reply = await host.send_message(bytes(body), request.headers.get("idempotency-key", "")) + return JSONResponse( + reply.data, status_code=reply.status, headers={"cache-control": "no-store"} + ) + try: + result = ( + await host.mint() + if operation == "token" + else await host.connect_number() + if operation == "connect" + else None + ) + except (PolymorfaError, ValueError, TypeError, KeyError, OverflowError): + return JSONResponse({"error": "browser_access_unavailable"}, status_code=502) + return JSONResponse( + result, + status_code=404 if result is None else 200, + headers={"cache-control": "no-store"}, + ) + + +@app.get("/api/polymorfa/conversation") +async def history(request: Request): + if not host.authenticated(request.cookies.get(COOKIE)): + return JSONResponse({"error": "sign_in_required"}, status_code=401) + return JSONResponse(host.snapshot(), headers={"cache-control": "no-store"}) + + +@app.post("/webhooks/polymorfa") +async def webhook(request: Request): + try: + event = await fastapi_webhook_event(request, host.webhook_secret, max_body_bytes=65536) + host.receive(event) + except (PolymorfaError, ValueError, TypeError, KeyError, OverflowError): + return JSONResponse({"error": "invalid_webhook"}, status_code=400) + return JSONResponse({"received": True}) diff --git a/examples/python/web_host/host.py b/examples/python/web_host/host.py new file mode 100644 index 00000000..0e2e824e --- /dev/null +++ b/examples/python/web_host/host.py @@ -0,0 +1,268 @@ +"""Shared loopback application policy; replace with live membership in your app.""" + +from __future__ import annotations + +import hashlib +import hmac +import json +import os +import re +import secrets +import threading +import time +from copy import deepcopy +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from urllib.parse import parse_qs, urlsplit + +from jinja2 import Environment, FileSystemLoader, select_autoescape + +from polymorfa import AsyncMessagingClient, Credential, PolymorfaError, RequestOptions, is_event + +ROOT = Path(__file__).resolve().parent +COOKIE = "polymorfa_example_session" +templates = Environment(loader=FileSystemLoader(ROOT / "templates"), autoescape=select_autoescape()) + + +@dataclass(frozen=True) +class Reply: + status: int + data: dict[str, object] + session: str | None = None + + +class Host: + def __init__(self) -> None: + self.username = os.environ["EXAMPLE_APP_USERNAME"] + self.password = hashlib.sha256(os.environ["EXAMPLE_APP_PASSWORD"].encode()).digest() + self.session = os.environ["POLYMORFA_SESSION"] + self.peer = os.environ["POLYMORFA_RECIPIENT"] + self.webhook_secret = os.environ["POLYMORFA_WEBHOOK_SECRET"] + if not self.peer or not self.webhook_secret: + raise ValueError("Configure the authorized recipient and webhook secret.") + self.transport = os.environ.get("POLYMORFA_EXAMPLE_TRANSPORT", "") + self.messages = [] + self.attempts = {} + self.event_ids = [] + self.received = 0 + self.last_event = "" + self.peer_id = "" + self.state_lock = threading.RLock() + self.send_lock = threading.Lock() + self.origin = os.environ.get("EXAMPLE_APP_ORIGIN", "http://127.0.0.1:8080") + self.api = os.environ.get("POLYMORFA_API_BASE_URL", "https://api.polymorfa.com") + self.credential = Credential("project_token", os.environ["POLYMORFA_PROJECT_TOKEN"]) + if not self.username or not self.session or not os.environ["EXAMPLE_APP_PASSWORD"]: + raise ValueError("Configure the example account and authorized number.") + if urlsplit(self.origin).hostname not in ("127.0.0.1", "localhost", "::1"): + raise ValueError("The example host is restricted to loopback.") + self.sessions: dict[str, float] = {} + + def authenticated(self, cookie: str | None) -> bool: + return cookie is not None and self.sessions.get(cookie, 0) > time.time() + + def login(self, body: bytes) -> Reply: + if len(body) > 16384: + return Reply(413, {"error": "invalid_sign_in"}) + values = parse_qs(body.decode("utf-8", "replace")) + username = values.get("username", [""])[0] + password = values.get("password", [""])[0] + if not ( + hmac.compare_digest( + hashlib.sha256(username.encode()).digest(), + hashlib.sha256(self.username.encode()).digest(), + ) + and hmac.compare_digest(hashlib.sha256(password.encode()).digest(), self.password) + ): + return Reply(401, {"error": "sign_in_failed"}) + self.sessions = { + key: expiry for key, expiry in self.sessions.items() if expiry > time.time() + } + if len(self.sessions) >= 128: + return Reply(429, {"error": "too_many_sessions"}) + session = secrets.token_urlsafe(32) + self.sessions[session] = time.time() + 1800 + return Reply(200, {"authenticated": True}, session) + + def logout(self, cookie: str | None) -> None: + if cookie is not None: + self.sessions.pop(cookie, None) + + def render(self, authenticated: bool, *, fragment: bool = False) -> str: + return templates.get_template("workspace.html" if fragment else "page.html").render( + authenticated=authenticated, transport=self.transport + ) + + async def mint(self) -> dict[str, object]: + async with AsyncMessagingClient(self.credential, base_url=self.api) as client: + response = await client.client_tokens.mint( + { + "ephemeralId": "example:local-app-user", + "session": self.session, + "ttlSeconds": 600, + } + ) + token = response.data["data"] + expiry = datetime.fromisoformat(token["expiresAt"].replace("Z", "+00:00")) + if not token["token"].startswith("pmfa_ct_") or expiry.tzinfo is None: + raise ValueError("Invalid browser grant response.") + return { + "value": token["token"], + "audience": "browser", + "expiresAt": int(expiry.timestamp() * 1000), + "scopes": ["read_messages", "connect_whatsapp"], + "grant": { + "session": self.session, + "conversations": [], + "allow": ["read_messages", "connect_whatsapp"], + }, + } + + async def connect_number(self) -> object: + async with AsyncMessagingClient(self.credential, base_url=self.api) as client: + result = await client.quick_links.create( + { + "purpose": "add_connection", + "session": self.session, + "addConnection": "linked_devices", + "connectionGoal": "hybrid", + } + ) + return result.data + + def snapshot(self): + with self.state_lock: + return deepcopy( + { + "messages": self.messages, + "peer": self.peer, + "session": self.session, + "receivedEvents": self.received, + "lastEvent": self.last_event, + } + ) + + async def send_message(self, body: bytes, key: str) -> Reply: + try: + value = json.loads(body) if len(body) <= 16384 else None + except (ValueError, UnicodeError): + value = None + if ( + not isinstance(value, dict) + or set(value) != {"text"} + or not isinstance(value["text"], str) + or not value["text"].strip() + or len(value["text"].encode()) > 4096 + or re.fullmatch(r"[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}", key) is None + ): + return Reply(400, {"error": "send_text_with_uuid_key"}) + if not self.send_lock.acquire(blocking=False): + return Reply(429, {"error": "send_in_progress"}) + try: + text = value["text"] + attempt = self.attempts.get(key) + if attempt: + if attempt["text"] != text: + return Reply(409, {"error": "key_has_different_text"}) + if attempt["message"]: + return Reply(200, deepcopy(attempt["message"])) + else: + if len(self.attempts) >= 128: + return Reply(429, {"error": "example_send_capacity_reached"}) + attempt = self.attempts[key] = {"text": text, "message": None} + try: + async with AsyncMessagingClient(self.credential, base_url=self.api) as client: + response = await client.messages.send( + self.session, + {"conversation": {"phoneNumber": self.peer}, "content": {"text": text}}, + options=RequestOptions(idempotency_key=key, timeout=60), + ) + receipt = response.data["data"] + if not response.data["success"] or not receipt["id"]: + raise ValueError("Invalid receipt") + at = datetime.fromisoformat(receipt["timestamp"].replace("Z", "+00:00")) + message = { + "id": receipt["id"], + "clientId": key, + "text": text, + "createdAt": int(at.timestamp()) * 1000, + "direction": "outbound", + "status": "sent", + "receipt": receipt["status"], + } + with self.state_lock: + self.peer_id = receipt["conversation"].get("id", "") or self.peer_id + self.upsert(message) + attempt["message"] = message + return Reply(200, deepcopy(message)) + except (PolymorfaError, ValueError, TypeError, KeyError, OverflowError): + # Retain the key on an unknown outcome. A retry uses the same API operation. + return Reply(502, {"error": "send_outcome_unknown_retry_same_key"}) + finally: + self.send_lock.release() + + def upsert(self, message): + self.messages = [m for m in self.messages if m["id"] != message["id"]] + [message] + self.messages = self.messages[-100:] + + def receive(self, event): + if event.session != self.session or not isinstance(event.payload, dict): + return + payload = event.payload + with self.state_lock: + if event.id in self.event_ids: + return + conversation = payload.get("conversation", {}) + assigned = ( + conversation.get("id") == self.peer_id + if self.peer_id + else conversation.get("phoneNumber", "").lstrip("+") == self.peer.lstrip("+") + ) + if not assigned: + return + if is_event(event, "message.received"): + text = payload.get("text", "") + if ( + payload.get("fromMe") is not False + or payload.get("isGroup") is not False + or not isinstance(text, str) + or not text + or len(text.encode()) > 4096 + or not payload.get("id") + or not isinstance(payload.get("timestamp"), (float, int)) + ): + return + self.upsert( + { + "id": payload["id"], + "text": text, + "createdAt": int(payload["timestamp"] * 1000), + "direction": "inbound", + "status": "sent", + "receipt": "received", + } + ) + elif is_event(event, "message.ack"): + ranks = { + "sent": 1, + "server": 1, + "delivered": 2, + "delivery": 2, + "read": 3, + "played": 3, + } + ids = {m.get("id") for m in payload.get("messages", []) if isinstance(m, dict)} + receipt = payload.get("type", "") + for message in self.messages: + if ( + message["id"] in ids + and message["direction"] == "outbound" + and ranks.get(receipt, 0) > ranks.get(message.get("receipt"), 0) + ): + message["receipt"] = receipt + else: + return + self.event_ids = (self.event_ids + [event.id])[-1024:] + self.received += 1 + self.last_event = event.event diff --git a/examples/python/web_host/templates/page.html b/examples/python/web_host/templates/page.html new file mode 100644 index 00000000..fcc1ccd1 --- /dev/null +++ b/examples/python/web_host/templates/page.html @@ -0,0 +1,71 @@ + + + + + + Python Polymorfa workspace + + + + + + {% include "workspace.html" %} + + diff --git a/examples/python/web_host/templates/workspace.html b/examples/python/web_host/templates/workspace.html new file mode 100644 index 00000000..106d7d06 --- /dev/null +++ b/examples/python/web_host/templates/workspace.html @@ -0,0 +1,46 @@ +
+

Messaging workspace

+ {% if authenticated %} {% if transport == "local-test-number" %} +

Local test numbers. Simulated WhatsApp transport.

+ {% endif %} +

+ Python SDK. Text messages use the server-assigned number and recipient. + Conversation state lasts until this example server stops. +

+

+ Waiting for the native SDK conversation. +

+
+ +
+ + {% else %} +

Sign in with the local example application's configured account.

+
+ + + +
+ {% endif %} + +
diff --git a/examples/rust/.gitignore b/examples/rust/.gitignore new file mode 100644 index 00000000..f4ceea78 --- /dev/null +++ b/examples/rust/.gitignore @@ -0,0 +1 @@ +**/target/ diff --git a/examples/rust/README.md b/examples/rust/README.md new file mode 100644 index 00000000..e3ca96db --- /dev/null +++ b/examples/rust/README.md @@ -0,0 +1,34 @@ +# Rust integration examples + +`axum` verifies the exact HTTP request bytes before decoding a webhook. Set +`POLYMORFA_WEBHOOK_SECRET`, then run: + +```sh +cargo run --manifest-path examples/rust/axum/Cargo.toml +``` + +The route returns `401` for a missing or invalid signature and `204` after +verification. Add durable event-ID deduplication and enqueueing for production. +Native signatures do not bind a timestamp. Use your deployment's HTTPS ingress +for incoming production webhooks; the example listens on loopback. + +Yew and Leptos applications keep this Rust SDK on the server. Their browser +frontends use the existing Polymorfa Web Components: + +```js +import { definePolymorfa } from "@polymorfa/elements"; +definePolymorfa({ tokenEndpoint: "/api/polymorfa/token" }); +``` + +Render ``, `` and `` +through the framework's custom-element support. There is no second Rust UI +implementation. The token endpoint must authenticate your application user, +derive their authorized session or customer from server state, and use +`MessagingClient.client_tokens().mint()` to issue a short-lived token. Never +accept the session/customer target or ephemeral identity as trusted browser +input, and never send a project token or team key to WASM. + +The runnable [Yew + Axum example](yew/README.md) builds a browser WASM application +with the existing Web Components and a server-owned authenticated token route. +Its native HTTP test proves login, origin checks, opaque cookies, server-derived +grants and logout. The frontend crate does not depend on the server SDK. diff --git a/examples/rust/axum/Cargo.lock b/examples/rust/axum/Cargo.lock new file mode 100644 index 00000000..e9eb56f9 --- /dev/null +++ b/examples/rust/axum/Cargo.lock @@ -0,0 +1,1886 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "num-traits", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.9", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "polymorfa-axum-webhook-example" +version = "0.1.0" +dependencies = [ + "axum", + "polymorfa-sdk", + "tokio", +] + +[[package]] +name = "polymorfa-sdk" +version = "0.1.0-dev.0" +dependencies = [ + "aes", + "async-stream", + "base64 0.22.1", + "bytes", + "cbc", + "chrono", + "flate2", + "futures-util", + "hex", + "hkdf", + "hmac", + "httpdate", + "percent-encoding", + "polymorfa-storage-core", + "rand 0.9.5", + "reqwest", + "serde", + "serde_json", + "sha2", + "subtle", + "tokio", + "tokio-tungstenite", + "tokio-util", + "url", + "uuid", +] + +[[package]] +name = "polymorfa-storage-core" +version = "0.1.0-dev.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "event-listener", + "futures-util", + "gloo-timers", + "js-sys", + "serde", + "serde_json", + "tokio", + "uuid", + "web-time", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.9", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "serde_json" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1741ab7a6cc54a03a89b5d563ed60075c277d9e3cfa73ad0c1f23f23974703c6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.7", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/rust/axum/Cargo.toml b/examples/rust/axum/Cargo.toml new file mode 100644 index 00000000..5cdee9b2 --- /dev/null +++ b/examples/rust/axum/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "polymorfa-axum-webhook-example" +version = "0.1.0" +edition = "2021" +rust-version = "1.85" +publish = false + +[dependencies] +axum = "0.8" +polymorfa-sdk = { path = "../../../packages/rust" } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] } diff --git a/examples/rust/axum/src/main.rs b/examples/rust/axum/src/main.rs new file mode 100644 index 00000000..7fb63721 --- /dev/null +++ b/examples/rust/axum/src/main.rs @@ -0,0 +1,68 @@ +use axum::{ + body::Bytes, + extract::State, + http::{HeaderMap, StatusCode}, + routing::post, + Router, +}; +use polymorfa_sdk::webhooks; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let secret = std::env::var("POLYMORFA_WEBHOOK_SECRET")?; + if secret.is_empty() { + return Err("POLYMORFA_WEBHOOK_SECRET must be nonempty".into()); + } + let app = Router::new() + .route("/webhooks/polymorfa", post(webhook)) + .with_state(Arc::new(secret)); + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?; + axum::serve(listener, app).await?; + Ok(()) +} + +async fn webhook(State(secret): State>, headers: HeaderMap, body: Bytes) -> StatusCode { + let Some(signature) = headers + .get("x-webhook-signature") + .and_then(|value| value.to_str().ok()) + else { + return StatusCode::UNAUTHORIZED; + }; + let Ok(event) = webhooks::construct_event(&body, signature, &secret) else { + return StatusCode::UNAUTHORIZED; + }; + // In production, atomically deduplicate event.envelope().id and enqueue the + // verified event before acknowledgement. Never log the raw body or secret. + let _verified_event = event; + StatusCode::NO_CONTENT +} + +#[cfg(test)] +mod tests { + use super::*; + #[tokio::test] + async fn raw_body_signature_is_required_and_verified_before_acknowledgement() { + let body=Bytes::from_static(br#"{"id":"evt_fixture","session":"support","timestamp":"2026-08-19T10:00:00Z","event":"future.ready","payload":{"capability":42}}"#); + let mut headers = HeaderMap::new(); + assert_eq!( + webhook( + State(Arc::new("fixture-secret".into())), + headers.clone(), + body.clone() + ) + .await, + StatusCode::UNAUTHORIZED + ); + headers.insert( + "x-webhook-signature", + "8b9682c1aee07cf45ce4b83983c2c224000ae4f8289c1ff38169bec4a52c6edd" + .parse() + .unwrap(), + ); + assert_eq!( + webhook(State(Arc::new("fixture-secret".into())), headers, body).await, + StatusCode::NO_CONTENT + ); + } +} diff --git a/examples/rust/yew/.gitignore b/examples/rust/yew/.gitignore new file mode 100644 index 00000000..96c33a45 --- /dev/null +++ b/examples/rust/yew/.gitignore @@ -0,0 +1,3 @@ +**/target/ +dist/ +frontend/public/elements.js diff --git a/examples/rust/yew/README.md b/examples/rust/yew/README.md new file mode 100644 index 00000000..3cf87a63 --- /dev/null +++ b/examples/rust/yew/README.md @@ -0,0 +1,78 @@ +# Yew with Axum and Polymorfa Web Components + +The Yew application renders the existing Polymorfa Web Components. Its WASM +binary has no dependency on the Rust server SDK. Axum owns the project token, +authenticates a local example user, and mints short-lived browser tokens for +that user's server-assigned WhatsApp session. The inbox uses native server sends. + +The inbox calls the native Rust SDK on Axum for text sends. Verified native +webhooks at `/webhooks/polymorfa` update its application-owned in-memory +conversation. Register `message.received` and `message.ack` for the assigned +Number with the configured signing secret. Incoming events must match the +Number and canonical conversation ID from the send receipt. This does not +enable HMS. + +The example retains at most 100 messages, 128 send attempts and 1024 recent +event IDs until the process stops. A UUID key belongs to one text; successful +replays do not send again and unknown outcomes retain the key for retry. +Display timestamps use whole seconds, matching native message events. Yew +restores the workspace from the authenticated server session after reload. +The separate token/connect endpoints remain browser-grant examples and are +not used by this inbox. + +Install Rust 1.85 or later, Trunk 0.21, and Node.js. From the SDK repository root: + +```sh +rustup target add wasm32-unknown-unknown +npm ci +node examples/rust/yew/build-elements.mjs +cd examples/rust/yew/frontend +trunk build --release +cd ../../../.. +cargo run --locked --manifest-path examples/rust/yew/server/Cargo.toml +``` + +Configure these environment variables in the server process before running it: + +| Variable | Purpose | +| ----------------------------- | -------------------------------------------------------- | +| `POLYMORFA_PROJECT_TOKEN` | Server project credential; never passed to WASM | +| `POLYMORFA_SESSION` | Session assigned by your application to the example user | +| `POLYMORFA_RECIPIENT` | Server-assigned recipient phone | +| `POLYMORFA_WEBHOOK_SECRET` | Native webhook signing secret | +| `POLYMORFA_EXAMPLE_TRANSPORT` | Set `local-test-number` to label simulated transport | +| `EXAMPLE_APP_USERNAME` | Local example login name | +| `EXAMPLE_APP_PASSWORD` | Local example login password | +| `POLYMORFA_EXAMPLE_PORT` | Loopback port, defaults to `3000` | +| `POLYMORFA_API_BASE_URL` | Optional API origin for local contract fixtures | + +Open `http://127.0.0.1:3000` (or the selected port). Sign in with the local +example credentials. The browser receives an opaque, HttpOnly, SameSite cookie; +it cannot choose the session, customer, or ephemeral ID sent to Polymorfa. +POST requests check the exact application origin. Logout revokes the server-side +application session immediately. Token responses are marked `no-store` and +include the grant consumed by `definePolymorfa`. + +The login and bounded in-memory session store are for this loopback example. +Replace them with your application's durable authentication and live +membership/assignment checks before deployment. Production cookies require +HTTPS and `Secure`. Preserve the server-derived session and user identity. +Do not replace authentication with a browser identity header or unsigned cookie. + +Verification: + +```sh +cargo test --locked --manifest-path examples/rust/yew/server/Cargo.toml +cargo clippy --locked --manifest-path examples/rust/yew/server/Cargo.toml --all-targets -- -D warnings +cargo check --locked --manifest-path examples/rust/yew/frontend/Cargo.toml --target wasm32-unknown-unknown +``` + +The native integration test exercises real Axum HTTP login, cookie validation, +logout, and the Rust SDK's authenticated token request. It verifies that foreign +origins, forged cookies, and browser-supplied session/identity fields cannot select +a different grant. Browser rendering and native API fixtures are separate from +live WhatsApp pairing or retained-history acceptance. + +Local Inspector video acceptance is separate from HTTP contract tests. It uses +paired simulated WhatsApp numbers, native Rust sends and signed HTTPS webhook +replies. Registry, staging and production availability remain separate gates. diff --git a/examples/rust/yew/build-elements.mjs b/examples/rust/yew/build-elements.mjs new file mode 100644 index 00000000..8541915b --- /dev/null +++ b/examples/rust/yew/build-elements.mjs @@ -0,0 +1,12 @@ +import { build } from "esbuild"; +import { fileURLToPath } from "node:url"; +await build({ + entryPoints: [fileURLToPath(new URL("frontend/bridge.js", import.meta.url))], + outfile: fileURLToPath( + new URL("frontend/public/elements.js", import.meta.url), + ), + bundle: true, + format: "esm", + platform: "browser", + target: "es2022", +}); diff --git a/examples/rust/yew/frontend/Cargo.lock b/examples/rust/yew/frontend/Cargo.lock new file mode 100644 index 00000000..bca738e3 --- /dev/null +++ b/examples/rust/yew/frontend/Cargo.lock @@ -0,0 +1,1126 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "boolinator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gloo" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console 0.2.3", + "gloo-dialogs 0.1.1", + "gloo-events 0.1.2", + "gloo-file 0.2.3", + "gloo-history 0.1.5", + "gloo-net 0.3.1", + "gloo-render 0.1.1", + "gloo-storage 0.2.2", + "gloo-timers 0.2.6", + "gloo-utils 0.1.7", + "gloo-worker 0.2.1", +] + +[[package]] +name = "gloo" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd35526c28cc55c1db77aed6296de58677dbab863b118483a27845631d870249" +dependencies = [ + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.4.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.4.0", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-console" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "gloo-events 0.1.2", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" +dependencies = [ + "gloo-events 0.2.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" +dependencies = [ + "gloo-events 0.1.2", + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_urlencoded", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" +dependencies = [ + "getrandom", + "gloo-events 0.2.0", + "gloo-utils 0.2.0", + "serde", + "serde-wasm-bindgen 0.6.5", + "serde_urlencoded", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.1.7", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 1.5.0", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode", + "gloo-console 0.2.3", + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76495d3dd87de51da268fa3a593da118ab43eb7f8809e17eb38d3319b424e400" +dependencies = [ + "bincode", + "futures", + "gloo-utils 0.2.0", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "implicit-clone" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84" +dependencies = [ + "implicit-clone-derive", + "indexmap", +] + +[[package]] +name = "implicit-clone-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pinned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures", + "rustversion", + "thiserror", +] + +[[package]] +name = "polymorfa-yew-browser-example" +version = "0.1.0" +dependencies = [ + "gloo-net 0.6.0", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prokio" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488" +dependencies = [ + "futures", + "gloo 0.8.1", + "num_cpus", + "once_cell", + "pin-project", + "pinned", + "tokio", + "tokio-stream", + "wasm-bindgen-futures", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "serde_json" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1741ab7a6cc54a03a89b5d563ed60075c277d9e3cfa73ad0c1f23f23974703c6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.7", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "yew" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac" +dependencies = [ + "console_error_panic_hook", + "futures", + "gloo 0.10.0", + "implicit-clone", + "indexmap", + "js-sys", + "prokio", + "rustversion", + "serde", + "slab", + "thiserror", + "tokio", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew-macro", +] + +[[package]] +name = "yew-macro" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2" +dependencies = [ + "boolinator", + "once_cell", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/rust/yew/frontend/Cargo.toml b/examples/rust/yew/frontend/Cargo.toml new file mode 100644 index 00000000..ef9a1788 --- /dev/null +++ b/examples/rust/yew/frontend/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "polymorfa-yew-browser-example" +version = "0.1.0" +edition = "2021" +rust-version = "1.85" +publish = false + +[dependencies] +yew = { version = "0.21", features = ["csr"] } +gloo-net = "0.6" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +wasm-bindgen = "=0.2.128" +wasm-bindgen-futures = "0.4" +web-sys = { version = "0.3", features = ["HtmlInputElement"] } diff --git a/examples/rust/yew/frontend/Trunk.toml b/examples/rust/yew/frontend/Trunk.toml new file mode 100644 index 00000000..925d3368 --- /dev/null +++ b/examples/rust/yew/frontend/Trunk.toml @@ -0,0 +1,3 @@ +[build] +target = "index.html" +dist = "../dist" diff --git a/examples/rust/yew/frontend/bridge.js b/examples/rust/yew/frontend/bridge.js new file mode 100644 index 00000000..1f99a6c8 --- /dev/null +++ b/examples/rust/yew/frontend/bridge.js @@ -0,0 +1,145 @@ +import { definePolymorfaElements } from "@polymorfa/elements"; +definePolymorfaElements(); +let disposeInbox; +// Only receipts the host observed from message.ack become chat receipts. The +// send response status is upstream acceptance, never delivery or read. +const observedReceipts = new Map([ + ["delivered", "delivered"], + ["delivery", "delivered"], + ["read", "read"], + ["played", "played"], +]); +function chatMessage({ receipt, ...message }) { + const state = + message.direction === "outbound" + ? observedReceipts.get(receipt) + : undefined; + return state ? { ...message, receipt: { state } } : message; +} +// The browser renders shared components. Every send and incoming message is +// handled by this application's native Rust SDK, not the browser API transport. +function nativeInbox(inbox) { + const changes = new Set(); + const messages = new Set(); + let previous = ""; + let stopped = false; + let timer; + const request = async (path, options = {}) => { + const response = await fetch(`/api/polymorfa/${path}`, { + credentials: "same-origin", + ...options, + }); + if (!response.ok) throw new Error(await response.text()); + return response.json(); + }; + const row = (snapshot) => { + const last = snapshot.messages.at(-1); + return { + id: snapshot.peer, + session: snapshot.session, + name: "Assigned recipient", + phoneNumber: snapshot.peer, + lastActivity: last?.createdAt ?? 0, + unreadCount: 0, + ...(last + ? { + lastMessage: { + text: last.text, + createdAt: last.createdAt, + direction: last.direction, + }, + } + : {}), + }; + }; + const poll = async () => { + try { + const snapshot = await request("conversation"); + if (stopped) return; + const serialized = JSON.stringify(snapshot); + if (serialized !== previous) { + previous = serialized; + for (const notify of changes) + notify({ type: "conversation", conversation: row(snapshot) }); + for (const message of snapshot.messages) + for (const notify of messages) + notify({ type: "upsert", message: chatMessage(message) }); + } + const label = document.querySelector("#transport-label"); + if (label) + label.textContent = snapshot.localTest + ? "Local test numbers. Simulated WhatsApp transport." + : ""; + const status = document.querySelector("#native-status"); + if (status) { + const outgoing = snapshot.messages.findLast( + (m) => m.direction === "outbound", + ); + status.textContent = + `Rust SDK · ${snapshot.receivedEvents} verified webhook events` + + (outgoing ? ` · Latest send: ${outgoing.receipt}` : ""); + } + } catch (error) { + const status = document.querySelector("#native-status"); + if (status && !stopped) status.textContent = error.message; + } finally { + if (!stopped) timer = setTimeout(poll, 800); + } + }; + inbox.source = { + async listConversations(_cursor, signal) { + return { + conversations: [row(await request("conversation", { signal }))], + }; + }, + subscribe(notify) { + changes.add(notify); + return () => changes.delete(notify); + }, + conversation() { + return { + async load(_cursor, signal) { + return { + messages: (await request("conversation", { signal })).messages.map( + chatMessage, + ), + }; + }, + subscribe(notify) { + messages.add(notify); + return () => messages.delete(notify); + }, + async send(message, signal) { + if (message.replyTo || message.attachments?.length) + throw new Error("This native example sends text only."); + return chatMessage( + await request("send", { + method: "POST", + signal, + headers: { + "Content-Type": "application/json", + "Idempotency-Key": message.clientId, + }, + body: JSON.stringify({ text: message.text }), + }), + ); + }, + }; + }, + }; + void poll(); + return () => { + stopped = true; + clearTimeout(timer); + changes.clear(); + messages.clear(); + }; +} +window.startPolymorfa = () => { + const inbox = document.querySelector("pmfa-inbox"); + if (inbox) disposeInbox ??= nativeInbox(inbox); +}; +window.stopPolymorfa = () => { + disposeInbox?.(); + disposeInbox = undefined; +}; diff --git a/examples/rust/yew/frontend/index.html b/examples/rust/yew/frontend/index.html new file mode 100644 index 00000000..73d51b33 --- /dev/null +++ b/examples/rust/yew/frontend/index.html @@ -0,0 +1,68 @@ + + + + + + Polymorfa Yew example + + + + + + + + diff --git a/examples/rust/yew/frontend/src/main.rs b/examples/rust/yew/frontend/src/main.rs new file mode 100644 index 00000000..5ab06ad8 --- /dev/null +++ b/examples/rust/yew/frontend/src/main.rs @@ -0,0 +1,122 @@ +use gloo_net::http::Request; +use serde::Serialize; +use wasm_bindgen::prelude::*; +use web_sys::HtmlInputElement; +use yew::prelude::*; +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace=window,js_name=startPolymorfa)] + fn start_polymorfa(); + #[wasm_bindgen(js_namespace=window,js_name=stopPolymorfa)] + fn stop_polymorfa(); +} +#[derive(Serialize)] +struct Login { + username: String, + password: String, +} +#[function_component(App)] +fn app() -> Html { + let username = use_state(String::new); + let password = use_state(String::new); + let logged_in = use_state(|| false); + let pending = use_state(|| false); + let error = use_state(String::new); + { + let logged_in = logged_in.clone(); + use_effect_with(*logged_in, move |signed_in| { + if *signed_in { + start_polymorfa(); + } + || {} + }); + } + { + let logged_in = logged_in.clone(); + use_effect_with((), move |_| { + wasm_bindgen_futures::spawn_local(async move { + if let Ok(response) = Request::get("/api/polymorfa/conversation").send().await { + if response.status() == 200 { + logged_in.set(true); + } + } + }); + || {} + }); + } + let onusername = { + let username = username.clone(); + Callback::from(move |event: InputEvent| { + username.set(event.target_unchecked_into::().value()); + }) + }; + let onpassword = { + let password = password.clone(); + Callback::from(move |event: InputEvent| { + password.set(event.target_unchecked_into::().value()); + }) + }; + let submit = { + let username = username.clone(); + let password = password.clone(); + let logged_in = logged_in.clone(); + let pending = pending.clone(); + let error = error.clone(); + Callback::from(move |event: SubmitEvent| { + event.prevent_default(); + if *pending { + return; + } + pending.set(true); + error.set(String::new()); + let input = Login { + username: (*username).clone(), + password: (*password).clone(), + }; + let logged_in = logged_in.clone(); + let pending = pending.clone(); + let error = error.clone(); + let password = password.clone(); + wasm_bindgen_futures::spawn_local(async move { + let response = Request::post("/api/login") + .header("content-type", "application/json") + .body(serde_json::to_string(&input).unwrap()) + .unwrap() + .send() + .await; + password.set(String::new()); + pending.set(false); + match response { + Ok(response) if response.status() == 204 => logged_in.set(true), + Ok(_) => { + error.set("Sign-in failed. Check your local example credentials.".into()) + } + Err(_) => error.set("Cannot reach the application server.".into()), + } + }); + }) + }; + let logout = { + let logged_in = logged_in.clone(); + let error = error.clone(); + Callback::from(move |_| { + let logged_in = logged_in.clone(); + let error = error.clone(); + wasm_bindgen_futures::spawn_local(async move { + match Request::post("/api/logout").send().await { + Ok(response) if response.status() == 204 => { + stop_polymorfa(); + logged_in.set(false); + } + _ => error.set( + "Cannot sign out. Retry when the application server is reachable.".into(), + ), + } + }); + }) + }; + html! {

{"Polymorfa in Yew"}

{if *logged_in {html!{

{(*error).clone()}

{"Rust SDK. Text messages use the server-assigned number and recipient. Conversation state lasts until this example server stops."}

}}else{html!{

{"Sign in to the local example"}

{(*error).clone()}

}}}
} +} +fn main() { + yew::Renderer::::new().render(); +} diff --git a/examples/rust/yew/server/Cargo.lock b/examples/rust/yew/server/Cargo.lock new file mode 100644 index 00000000..225c81df --- /dev/null +++ b/examples/rust/yew/server/Cargo.lock @@ -0,0 +1,1925 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "num-traits", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.9", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "polymorfa-sdk" +version = "0.1.0-dev.0" +dependencies = [ + "aes", + "async-stream", + "base64 0.22.1", + "bytes", + "cbc", + "chrono", + "flate2", + "futures-util", + "hex", + "hkdf", + "hmac", + "httpdate", + "percent-encoding", + "polymorfa-storage-core", + "rand 0.9.5", + "reqwest", + "serde", + "serde_json", + "sha2", + "subtle", + "tokio", + "tokio-tungstenite", + "tokio-util", + "url", + "uuid", +] + +[[package]] +name = "polymorfa-storage-core" +version = "0.1.0-dev.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "event-listener", + "futures-util", + "gloo-timers", + "js-sys", + "serde", + "serde_json", + "tokio", + "uuid", + "web-time", +] + +[[package]] +name = "polymorfa-yew-token-server" +version = "0.1.0" +dependencies = [ + "axum", + "chrono", + "polymorfa-sdk", + "reqwest", + "serde", + "serde_json", + "sha2", + "subtle", + "tokio", + "tower-http", + "uuid", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.9", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "serde_json" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1741ab7a6cc54a03a89b5d563ed60075c277d9e3cfa73ad0c1f23f23974703c6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.7", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/rust/yew/server/Cargo.toml b/examples/rust/yew/server/Cargo.toml new file mode 100644 index 00000000..6bcdaac2 --- /dev/null +++ b/examples/rust/yew/server/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "polymorfa-yew-token-server" +version = "0.1.0" +edition = "2021" +rust-version = "1.85" +publish = false + +[dependencies] +axum = "0.8" +chrono = { version = "0.4", default-features = false, features = ["std"] } +polymorfa-sdk = { path = "../../../../packages/rust" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +subtle = "2" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "sync"] } +tower-http = { version = "0.6", features = ["fs"] } +uuid = { version = "1", features = ["v4"] } + +[dev-dependencies] +reqwest = { version = "0.12", default-features = false, features = ["json"] } diff --git a/examples/rust/yew/server/src/conversation.rs b/examples/rust/yew/server/src/conversation.rs new file mode 100644 index 00000000..4aeadcb8 --- /dev/null +++ b/examples/rust/yew/server/src/conversation.rs @@ -0,0 +1,199 @@ +use super::*; +use polymorfa_sdk::{ + models::{ConversationReference, SendMessageRequest}, + webhooks::{ + self, EventConversation, KnownWebhookPayload, MessageReceivedPayload, WebhookEvent, + }, + RequestOptions, +}; +use serde_json::{json, Value}; +use std::collections::VecDeque; + +#[derive(Default)] +pub struct Conversation { + pub messages: Vec, + pub attempts: HashMap)>, + pub peer_id: String, + pub event_ids: VecDeque, + pub received: usize, + pub last_event: String, +} +impl Conversation { + fn upsert(&mut self, message: Value) { + if let Some(prior) = self.messages.iter_mut().find(|m| m["id"] == message["id"]) { + *prior = message; + } else { + self.messages.push(message); + } + if self.messages.len() > 100 { + self.messages.remove(0); + } + } + fn assigned(&self, peer: &str, c: &EventConversation) -> bool { + if self.peer_id.is_empty() { + c.phone_number + .as_deref() + .unwrap_or("") + .trim_start_matches('+') + == peer.trim_start_matches('+') + } else { + c.id == self.peer_id + } + } +} +pub async fn snapshot(State(app): State>, headers: HeaderMap) -> Response { + if authenticated(&app, &headers).is_none() { + return StatusCode::UNAUTHORIZED.into_response(); + } + let s = app.conversation.lock().unwrap(); + ([(header::CACHE_CONTROL,"no-store")],Json(json!({"messages":s.messages,"peer":app.peer,"session":app.session,"receivedEvents":s.received,"lastEvent":s.last_event,"localTest":app.local_test}))).into_response() +} +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct SendInput { + text: String, +} +pub async fn send(State(app): State>, headers: HeaderMap, body: Bytes) -> Response { + if !trusted_origin(&app, &headers) { + return StatusCode::FORBIDDEN.into_response(); + } + if authenticated(&app, &headers).is_none() { + return StatusCode::UNAUTHORIZED.into_response(); + } + let Ok(input) = serde_json::from_slice::(&body) else { + return StatusCode::BAD_REQUEST.into_response(); + }; + let key = headers + .get("idempotency-key") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + if body.len() > 16384 + || input.text.trim().is_empty() + || input.text.len() > 4096 + || key.len() != 36 + || uuid::Uuid::parse_str(key).is_err() + { + return StatusCode::BAD_REQUEST.into_response(); + } + let Ok(_gate) = app.send_gate.try_lock() else { + return StatusCode::TOO_MANY_REQUESTS.into_response(); + }; + { + let mut s = app.conversation.lock().unwrap(); + if let Some((text, message)) = s.attempts.get(key) { + if text != &input.text { + return StatusCode::CONFLICT.into_response(); + } + if let Some(message) = message { + return Json(message.clone()).into_response(); + } + } else { + if s.attempts.len() >= 128 { + return StatusCode::TOO_MANY_REQUESTS.into_response(); + } + s.attempts.insert(key.into(), (input.text.clone(), None)); + } + } + let response = app + .client + .messages() + .send( + &app.session, + &SendMessageRequest::text(ConversationReference::phone(&app.peer), &input.text), + RequestOptions { + idempotency_key: Some(key.into()), + timeout: Some(Duration::from_secs(60)), + ..Default::default() + }, + ) + .await; + let Ok(result) = response else { + return ( + StatusCode::BAD_GATEWAY, + "Send outcome unknown. Retry the same text and key.", + ) + .into_response(); + }; + let receipt = result.data.data.receipt; + if !result.data.success || receipt.id.is_empty() { + return StatusCode::BAD_GATEWAY.into_response(); + } + let Ok(at) = chrono::DateTime::parse_from_rfc3339(&receipt.timestamp) else { + return StatusCode::BAD_GATEWAY.into_response(); + }; + let message = json!({"id":receipt.id,"clientId":key,"text":input.text,"createdAt":at.timestamp()*1000,"direction":"outbound","status":"sent","receipt":receipt.status}); + let mut s = app.conversation.lock().unwrap(); + if !receipt.conversation.id.is_empty() { + s.peer_id = receipt.conversation.id; + } + s.upsert(message.clone()); + s.attempts.get_mut(key).unwrap().1 = Some(message.clone()); + ([(header::CACHE_CONTROL, "no-store")], Json(message)).into_response() +} +fn rank(receipt: &str) -> u8 { + match receipt { + "read" | "played" => 3, + "delivered" | "delivery" => 2, + "sent" | "server" => 1, + _ => 0, + } +} +pub async fn webhook(State(app): State>, headers: HeaderMap, body: Bytes) -> Response { + let signature = headers + .get("x-webhook-signature") + .and_then(|h| h.to_str().ok()) + .unwrap_or(""); + let Ok(event) = webhooks::construct_event(&body, signature, &app.webhook_secret) else { + return StatusCode::BAD_REQUEST.into_response(); + }; + let envelope = event.envelope(); + if envelope.session != app.session { + return Json(json!({"received":true})).into_response(); + } + let mut s = app.conversation.lock().unwrap(); + if s.event_ids.contains(&envelope.id) { + return Json(json!({"received":true})).into_response(); + } + match &event { + WebhookEvent::MessageReceived { + payload: MessageReceivedPayload::LinkedDevices(p), + .. + } => { + let text = p.text.as_deref().unwrap_or(""); + if p.from_me + || p.is_group + || text.is_empty() + || text.len() > 4096 + || p.id.is_empty() + || !s.assigned(&app.peer, &p.conversation) + { + return Json(json!({"received":true})).into_response(); + } + s.upsert(json!({"id":p.id,"text":text,"createdAt":p.timestamp.saturating_mul(1000),"direction":"inbound","status":"sent","receipt":"received"})); + } + WebhookEvent::Known { + payload: KnownWebhookPayload::MessageAck(p), + .. + } => { + if !s.assigned(&app.peer, &p.conversation) { + return Json(json!({"received":true})).into_response(); + } + for m in &mut s.messages { + if m["direction"] == "outbound" + && p.messages.iter().any(|r| m["id"] == r.id) + && rank(&p.ack_type) > rank(m["receipt"].as_str().unwrap_or("")) + { + m["receipt"] = json!(p.ack_type); + } + } + } + _ => return Json(json!({"received":true})).into_response(), + } + s.received += 1; + s.last_event = envelope.event.clone(); + s.event_ids.push_back(envelope.id.clone()); + if s.event_ids.len() > 1024 { + s.event_ids.pop_front(); + } + Json(json!({"received":true})).into_response() +} diff --git a/examples/rust/yew/server/src/main.rs b/examples/rust/yew/server/src/main.rs new file mode 100644 index 00000000..d5892389 --- /dev/null +++ b/examples/rust/yew/server/src/main.rs @@ -0,0 +1,282 @@ +//! Loopback integration example. Replace the local login/session store with +//! your application's authentication and live authorization before deployment. +use axum::{ + body::Bytes, + extract::{DefaultBodyLimit, State}, + http::{header, HeaderMap, StatusCode}, + response::{IntoResponse, Response}, + routing::{get, post}, + Json, Router, +}; +use polymorfa_sdk::{ + connections::{ClientTokenTarget, MintClientTokenRequest}, + ClientOptions, Credential, MessagingClient, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; +use subtle::ConstantTimeEq; +use tower_http::services::ServeDir; +mod conversation; +struct App { + peer: String, + webhook_secret: String, + local_test: bool, + conversation: Mutex, + send_gate: tokio::sync::Mutex<()>, + client: MessagingClient, + username_hash: [u8; 32], + password_hash: [u8; 32], + user_id: String, + session: String, + origin: String, + users: Mutex>, +} +struct AppSession { + user_id: String, + expires: Instant, +} +#[derive(Deserialize)] +struct Login { + username: String, + password: String, +} +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct BrowserToken { + value: String, + audience: &'static str, + expires_at: i64, + scopes: Vec<&'static str>, + grant: BrowserGrant, +} +#[derive(Serialize)] +struct BrowserGrant { + session: String, + conversations: Vec, + allow: Vec<&'static str>, +} +fn digest(value: &str) -> [u8; 32] { + Sha256::digest(value.as_bytes()).into() +} +fn router(app: Arc) -> Router { + Router::new() + .route("/api/login", post(login)) + .route("/api/logout", post(logout)) + .route("/api/polymorfa/token", post(token)) + .route("/api/polymorfa/conversation", get(conversation::snapshot)) + .route("/api/polymorfa/send", post(conversation::send)) + .route( + "/webhooks/polymorfa", + post(conversation::webhook).layer(DefaultBodyLimit::max(65536)), + ) + .route("/api/polymorfa/connect", post(connect)) + .layer(DefaultBodyLimit::max(16 * 1024)) + .fallback_service(ServeDir::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../dist" + ))) + .with_state(app) +} +fn trusted_origin(app: &App, headers: &HeaderMap) -> bool { + headers.get(header::ORIGIN).and_then(|h| h.to_str().ok()) == Some(app.origin.as_str()) +} +fn authenticated(app: &App, headers: &HeaderMap) -> Option { + cookie(headers).and_then(|id| { + app.users + .lock() + .unwrap() + .get(id) + .filter(|s| s.expires > Instant::now()) + .map(|s| s.user_id.clone()) + }) +} +fn cookie(headers: &HeaderMap) -> Option<&str> { + headers + .get(header::COOKIE)? + .to_str() + .ok()? + .split(';') + .find_map(|part| part.trim().strip_prefix("polymorfa_example_session=")) +} +async fn login( + State(app): State>, + headers: HeaderMap, + Json(input): Json, +) -> Response { + if !trusted_origin(&app, &headers) { + return StatusCode::FORBIDDEN.into_response(); + } + let valid = digest(&input.username).ct_eq(&app.username_hash) + & digest(&input.password).ct_eq(&app.password_hash); + if !bool::from(valid) { + return StatusCode::UNAUTHORIZED.into_response(); + } + let mut users = app.users.lock().unwrap(); + users.retain(|_, session| session.expires > Instant::now()); + if users.len() >= 128 { + return StatusCode::TOO_MANY_REQUESTS.into_response(); + } + let id = uuid::Uuid::new_v4().to_string(); + let user_id = app.user_id.clone(); + users.insert( + id.clone(), + AppSession { + user_id, + expires: Instant::now() + Duration::from_secs(1800), + }, + ); + ([(header::SET_COOKIE,format!("polymorfa_example_session={id}; HttpOnly; SameSite=Strict; Path=/; Max-Age=1800")),(header::CACHE_CONTROL,"no-store".into())],StatusCode::NO_CONTENT).into_response() +} +async fn logout(State(app): State>, headers: HeaderMap) -> Response { + if !trusted_origin(&app, &headers) { + return StatusCode::FORBIDDEN.into_response(); + } + if let Some(id) = cookie(&headers) { + app.users.lock().unwrap().remove(id); + } + ( + [( + header::SET_COOKIE, + "polymorfa_example_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0", + )], + StatusCode::NO_CONTENT, + ) + .into_response() +} +async fn connect(State(app): State>, headers: HeaderMap) -> Response { + if !trusted_origin(&app, &headers) { + return StatusCode::FORBIDDEN.into_response(); + } + if authenticated(&app, &headers).is_none() { + return StatusCode::UNAUTHORIZED.into_response(); + } + let request = polymorfa_sdk::models::CreateQuickLinkRequest { + purpose: Some(polymorfa_sdk::models::CreateQuickLinkPurpose::AddConnection), + session: Some(app.session.clone()), + add_connection: Some(polymorfa_sdk::models::ConnectionKind::LinkedDevices), + connection_goal: Some(polymorfa_sdk::models::ConnectionGoal::Hybrid), + ..Default::default() + }; + match app.client.quick_links().create(&request,Default::default()).await {Ok(result)=>([(header::CACHE_CONTROL,"no-store")],Json(serde_json::json!({"data":{"id":result.data.data.id,"url":result.data.data.url,"expiresAt":result.data.data.expires_at}}))).into_response(),Err(_)=>StatusCode::BAD_GATEWAY.into_response()} +} +async fn token( + State(app): State>, + headers: HeaderMap, + _untrusted_body: Bytes, +) -> Response { + if !trusted_origin(&app, &headers) { + return StatusCode::FORBIDDEN.into_response(); + } + let user = cookie(&headers).and_then(|id| { + app.users + .lock() + .unwrap() + .get(id) + .filter(|s| s.expires > Instant::now()) + .map(|s| s.user_id.clone()) + }); + let Some(user) = user else { + return StatusCode::UNAUTHORIZED.into_response(); + }; + // Both identity and number assignment are server-owned. No browser body or + // identity header chooses a session, customer, or ephemeral ID. + let result = app + .client + .client_tokens() + .mint( + &MintClientTokenRequest { + ephemeral_id: format!("example:{user}"), + ttl_seconds: Some(600), + target: ClientTokenTarget::Session { + session: app.session.clone(), + allow: None, + }, + }, + Default::default(), + ) + .await; + match result { + Ok(result) => { + let value = result.data.data; + if !value.token.starts_with("pmfa_ct_") || value.token.len() <= 8 { + return StatusCode::BAD_GATEWAY.into_response(); + } + let Ok(expiry) = chrono::DateTime::parse_from_rfc3339(&value.expires_at) else { + return StatusCode::BAD_GATEWAY.into_response(); + }; + ( + [(header::CACHE_CONTROL, "no-store")], + Json(BrowserToken { + value: value.token, + audience: "browser", + expires_at: expiry.timestamp_millis(), + scopes: vec!["read_messages", "connect_whatsapp"], + grant: BrowserGrant { + session: app.session.clone(), + conversations: Vec::new(), + allow: vec!["read_messages", "connect_whatsapp"], + }, + }), + ) + .into_response() + } + Err(error) => { + let status = if error.status.is_some_and(|s| (400..500).contains(&s)) { + StatusCode::FORBIDDEN + } else { + StatusCode::BAD_GATEWAY + }; + status.into_response() + } + } +} +#[tokio::main] +async fn main() -> Result<(), Box> { + let username = std::env::var("EXAMPLE_APP_USERNAME")?; + let password = std::env::var("EXAMPLE_APP_PASSWORD")?; + let session = std::env::var("POLYMORFA_SESSION")?; + let peer = std::env::var("POLYMORFA_RECIPIENT")?; + let webhook_secret = std::env::var("POLYMORFA_WEBHOOK_SECRET")?; + if username.is_empty() + || password.is_empty() + || session.is_empty() + || peer.is_empty() + || webhook_secret.is_empty() + { + return Err("Set a nonempty example username, password, and authorized session".into()); + } + let credential = Credential::project_token(std::env::var("POLYMORFA_PROJECT_TOKEN")?)?; + let port: u16 = std::env::var("POLYMORFA_EXAMPLE_PORT") + .unwrap_or_else(|_| "3000".into()) + .parse()?; + let client_options = ClientOptions { + base_url: std::env::var("POLYMORFA_API_BASE_URL") + .unwrap_or_else(|_| ClientOptions::default().base_url), + ..Default::default() + }; + let app = Arc::new(App { + client: MessagingClient::new(credential, client_options)?, + username_hash: digest(&username), + password_hash: digest(&password), + user_id: uuid::Uuid::new_v4().to_string(), + session, + origin: format!("http://127.0.0.1:{port}"), + users: Mutex::new(HashMap::new()), + peer, + webhook_secret, + local_test: std::env::var("POLYMORFA_EXAMPLE_TRANSPORT").as_deref() + == Ok("local-test-number"), + conversation: Mutex::new(Default::default()), + send_gate: tokio::sync::Mutex::new(()), + }); + let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await?; + axum::serve(listener, router(app)).await?; + Ok(()) +} +#[cfg(test)] +mod tests; diff --git a/examples/rust/yew/server/src/tests.rs b/examples/rust/yew/server/src/tests.rs new file mode 100644 index 00000000..30a69b89 --- /dev/null +++ b/examples/rust/yew/server/src/tests.rs @@ -0,0 +1,455 @@ +use super::*; +use serde_json::json; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +#[tokio::test] +async fn real_app_login_uses_opaque_cookie_and_server_assignment_for_native_token_mint() { + let api = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let api_url = format!("http://{}", api.local_addr().unwrap()); + let upstream = tokio::spawn(async move { + let (mut stream, _) = api.accept().await.unwrap(); + let mut bytes = Vec::new(); + let mut buffer = [0; 4096]; + let (end, length) = loop { + let n = stream.read(&mut buffer).await.unwrap(); + assert!(n > 0); + bytes.extend_from_slice(&buffer[..n]); + if let Some(end) = bytes.windows(4).position(|w| w == b"\r\n\r\n") { + let head = String::from_utf8_lossy(&bytes[..end]); + let length = head + .lines() + .find_map(|l| { + l.to_ascii_lowercase() + .strip_prefix("content-length:") + .and_then(|s| s.trim().parse::().ok()) + }) + .unwrap(); + break (end + 4, length); + } + }; + while bytes.len() < end + length { + let n = stream.read(&mut buffer).await.unwrap(); + assert!(n > 0); + bytes.extend_from_slice(&buffer[..n]); + } + let head = String::from_utf8_lossy(&bytes[..end]); + assert!(head.starts_with("POST /platform/client-tokens HTTP/1.1")); + assert!(head.to_lowercase().contains("authorization: bearer pmfa_")); + assert_eq!( + serde_json::from_slice::(&bytes[end..]).unwrap(), + json!({"ephemeralId":"example:app-user-id","session":"authorized-support","ttlSeconds":600}) + ); + let body=json!({"success":true,"data":{"token":"pmfa_ct_YnJvd3Nlcl9maXh0dXJl","expiresAt":"2030-01-01T00:00:00Z"}}).to_string(); + stream.write_all(format!("HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",body.len()).as_bytes()).await.unwrap(); + }); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let origin = format!("http://{}", listener.local_addr().unwrap()); + let app = Arc::new(App { + client: MessagingClient::new( + Credential::organization_api_key("pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k").unwrap(), + ClientOptions { + base_url: api_url, + max_network_retries: 0, + ..Default::default() + }, + ) + .unwrap(), + username_hash: digest("example-user"), + password_hash: digest("test-only-password"), + user_id: "app-user-id".into(), + session: "authorized-support".into(), + origin: origin.clone(), + users: Mutex::new(HashMap::new()), + peer: "+15550001234".into(), + webhook_secret: "local-test-signing-key".into(), + local_test: true, + conversation: Mutex::new(Default::default()), + send_gate: tokio::sync::Mutex::new(()), + }); + let router = router(app.clone()); + let server = tokio::spawn(async move { + axum::serve(listener, router).await.unwrap(); + }); + let client = reqwest::Client::new(); + assert_eq!( + client + .post(format!("{origin}/api/polymorfa/token")) + .header("origin", &origin) + .send() + .await + .unwrap() + .status(), + StatusCode::UNAUTHORIZED + ); + assert_eq!( + client + .post(format!("{origin}/api/polymorfa/token")) + .header("origin", "https://evil.invalid") + .send() + .await + .unwrap() + .status(), + StatusCode::FORBIDDEN + ); + assert_eq!( + client + .post(format!("{origin}/api/login")) + .header("origin", &origin) + .json(&json!({"username":"example-user","password":"wrong"})) + .send() + .await + .unwrap() + .status(), + StatusCode::UNAUTHORIZED + ); + let login = client + .post(format!("{origin}/api/login")) + .header("origin", &origin) + .json(&json!({"username":"example-user","password":"test-only-password"})) + .send() + .await + .unwrap(); + assert_eq!(login.status(), StatusCode::NO_CONTENT); + let cookie = login.headers()["set-cookie"].to_str().unwrap().to_owned(); + assert!(cookie.contains("HttpOnly; SameSite=Strict")); + let cookie = cookie.split(';').next().unwrap(); + assert!(!cookie.contains("app-user-id")); + assert_eq!( + client + .post(format!("{origin}/api/polymorfa/token")) + .header("origin", &origin) + .header("cookie", "polymorfa_example_session=forged") + .send() + .await + .unwrap() + .status(), + StatusCode::UNAUTHORIZED + ); + let minted = client + .post(format!("{origin}/api/polymorfa/token")) + .header("origin", &origin) + .header("cookie", cookie) + .header("x-example-authenticated-user", "evil") + .json(&json!({"session":"foreign","customer":"foreign","ephemeralId":"evil"})) + .send() + .await + .unwrap(); + assert_eq!(minted.status(), StatusCode::OK); + assert_eq!(minted.headers()["cache-control"], "no-store"); + assert_eq!( + minted.json::().await.unwrap(), + json!({"value":"pmfa_ct_YnJvd3Nlcl9maXh0dXJl","audience":"browser","expiresAt":1893456000000i64,"scopes":["read_messages","connect_whatsapp"],"grant":{"session":"authorized-support","conversations":[],"allow":["read_messages","connect_whatsapp"]}}) + ); + upstream.await.unwrap(); + assert_eq!( + client + .post(format!("{origin}/api/logout")) + .header("origin", &origin) + .header("cookie", cookie) + .send() + .await + .unwrap() + .status(), + StatusCode::NO_CONTENT + ); + assert_eq!( + client + .post(format!("{origin}/api/polymorfa/token")) + .header("origin", &origin) + .header("cookie", cookie) + .send() + .await + .unwrap() + .status(), + StatusCode::UNAUTHORIZED + ); + assert!(app.users.lock().unwrap().is_empty()); + server.abort(); +} +#[tokio::test] +async fn connect_and_conversation_require_app_session_and_keep_assignment_on_server() { + let api = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let api_url = format!("http://{}", api.local_addr().unwrap()); + let upstream = tokio::spawn(async move { + let (mut stream, _) = api.accept().await.unwrap(); + let mut bytes = Vec::new(); + let mut buffer = [0; 4096]; + loop { + let n = stream.read(&mut buffer).await.unwrap(); + assert!(n > 0); + bytes.extend_from_slice(&buffer[..n]); + if let Some(end) = bytes.windows(4).position(|w| w == b"\r\n\r\n") { + let head = String::from_utf8_lossy(&bytes[..end]); + let length = head + .lines() + .find_map(|l| { + l.to_lowercase() + .strip_prefix("content-length:") + .and_then(|v| v.trim().parse::().ok()) + }) + .unwrap_or(0); + if bytes.len() >= end + 4 + length { + assert!(head.starts_with("POST /messaging/quicklinks HTTP/1.1")); + assert_eq!( + serde_json::from_slice::(&bytes[end + 4..]).unwrap(), + json!({"purpose":"add_connection","session":"authorized-support","addConnection":"linked_devices","connectionGoal":"hybrid"}) + ); + break; + } + } + } + let body=json!({"success":true,"data":{"purpose":"add_connection","connectionGoal":"hybrid","addConnection":"linked_devices","id":"link","url":"https://quick.example.com/link","session":"authorized-support","expiresAt":null}}).to_string(); + stream.write_all(format!("HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",body.len()).as_bytes()).await.unwrap(); + }); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let origin = format!("http://{}", listener.local_addr().unwrap()); + let app = Arc::new(App { + client: MessagingClient::new( + Credential::organization_api_key("pmfa_AAECAwQFBgcICQoLDA0ODxAREhM73f-k").unwrap(), + ClientOptions { + base_url: api_url, + max_network_retries: 0, + ..Default::default() + }, + ) + .unwrap(), + username_hash: digest("user"), + password_hash: digest("fixture"), + user_id: "app-user".into(), + session: "authorized-support".into(), + origin: origin.clone(), + peer: "+15550001234".into(), + webhook_secret: "local-test-signing-key".into(), + local_test: true, + conversation: Mutex::new(Default::default()), + send_gate: tokio::sync::Mutex::new(()), + users: Mutex::new(HashMap::from([( + "opaque-app-session".into(), + AppSession { + user_id: "app-user".into(), + expires: Instant::now() + Duration::from_secs(60), + }, + )])), + }); + let server = tokio::spawn(async move { + axum::serve(listener, router(app)).await.unwrap(); + }); + let client = reqwest::Client::new(); + assert_eq!( + client + .get(format!("{origin}/api/polymorfa/conversation")) + .send() + .await + .unwrap() + .status(), + StatusCode::UNAUTHORIZED + ); + assert_eq!( + client + .get(format!("{origin}/api/polymorfa/conversation")) + .header("cookie", "polymorfa_example_session=opaque-app-session") + .send() + .await + .unwrap() + .json::() + .await + .unwrap(), + json!({"messages":[],"peer":"+15550001234","session":"authorized-support","receivedEvents":0,"lastEvent":"","localTest":true}) + ); + assert_eq!( + client + .post(format!("{origin}/api/polymorfa/connect")) + .header("origin", &origin) + .send() + .await + .unwrap() + .status(), + StatusCode::UNAUTHORIZED + ); + let link = client + .post(format!("{origin}/api/polymorfa/connect")) + .header("origin", &origin) + .header("cookie", "polymorfa_example_session=opaque-app-session") + .json(&json!({"session":"foreign"})) + .send() + .await + .unwrap(); + assert_eq!(link.status(), StatusCode::OK); + assert_eq!( + link.json::().await.unwrap(), + json!({"data":{"id":"link","url":"https://quick.example.com/link","expiresAt":null}}) + ); + upstream.await.unwrap(); + server.abort(); +} + +#[tokio::test] +async fn native_inbox_sends_once_and_consumes_signed_peer_reply_after_reload() { + use std::sync::atomic::{AtomicUsize, Ordering}; + let sends = Arc::new(AtomicUsize::new(0)); + let count = sends.clone(); + let upstream = Router::new().route("/messaging/authorized-support/messages/send", post(move |headers: HeaderMap, Json(body): Json| { + let count=count.clone();async move { + assert_eq!(headers["idempotency-key"],"ad87d91a-32fc-48ab-aa00-5973d1e9f98d"); + assert_eq!(body,json!({"conversation":{"phoneNumber":"+15550001234"},"content":{"text":"Native send"}})); + count.fetch_add(1,Ordering::SeqCst); + Json(json!({"success":true,"data":{"id":"message-out","timestamp":"2026-10-18T10:00:00.750Z","status":"sent","type":"text","conversation":{"id":"canonical-peer"},"whatsapp_ids":{"linked_devices":"wire-out"}}})) + } + })); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let api_url = format!("http://{}", listener.local_addr().unwrap()); + let api = tokio::spawn(async move { + axum::serve(listener, upstream).await.unwrap(); + }); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let origin = format!("http://{}", listener.local_addr().unwrap()); + let app = Arc::new(App { + client: MessagingClient::new( + Credential::project_token("pmfa_pt_AAECAwQFBgcICQoLDA0ODxAREhM73f-k").unwrap(), + ClientOptions { + base_url: api_url, + max_network_retries: 0, + ..Default::default() + }, + ) + .unwrap(), + username_hash: digest("example-user"), + password_hash: digest("test-only-password"), + user_id: "app-user-id".into(), + session: "authorized-support".into(), + origin: origin.clone(), + users: Mutex::new(HashMap::new()), + peer: "+15550001234".into(), + webhook_secret: "local-test-signing-key".into(), + local_test: true, + conversation: Mutex::new(Default::default()), + send_gate: tokio::sync::Mutex::new(()), + }); + let app_router = router(app); + let server = tokio::spawn(async move { + axum::serve(listener, app_router).await.unwrap(); + }); + let client = reqwest::Client::new(); + let login = client + .post(format!("{origin}/api/login")) + .header("origin", &origin) + .json(&json!({"username":"example-user","password":"test-only-password"})) + .send() + .await + .unwrap(); + let cookie = login.headers()["set-cookie"] + .to_str() + .unwrap() + .split(';') + .next() + .unwrap() + .to_string(); + let send = |body: serde_json::Value| { + client + .post(format!("{origin}/api/polymorfa/send")) + .header("origin", &origin) + .header("cookie", &cookie) + .header("idempotency-key", "ad87d91a-32fc-48ab-aa00-5973d1e9f98d") + .json(&body) + }; + assert_eq!( + send(json!({"text":"Native send","recipient":"foreign"})) + .send() + .await + .unwrap() + .status(), + StatusCode::BAD_REQUEST + ); + assert_eq!( + send(json!({"text":"Native send"})) + .send() + .await + .unwrap() + .status(), + StatusCode::OK + ); + assert_eq!( + send(json!({"text":"Native send"})) + .send() + .await + .unwrap() + .status(), + StatusCode::OK + ); + assert_eq!( + send(json!({"text":"Changed"})) + .send() + .await + .unwrap() + .status(), + StatusCode::CONFLICT + ); + assert_eq!(sends.load(Ordering::SeqCst), 1); + let event = polymorfa_sdk::webhooks::WebhookEvent::Unknown( + polymorfa_sdk::webhooks::WebhookEnvelope { + id: "event-1".into(), + session: "authorized-support".into(), + timestamp: "2026-10-18T10:00:01Z".into(), + event: "message.received".into(), + external_id: None, + payload: json!({"id":"message-in","conversation":{"id":"canonical-peer"},"fromMe":false,"isGroup":false,"timestamp":1792317601,"text":"Independent reply","type":"text","pushName":"Test peer","whatsapp_ids":{"linked_devices":"wire-in"}}), + }, + ); + let fixture = + polymorfa_sdk::webhooks::create_fixture(&event, "local-test-signing-key").unwrap(); + let webhook = |signature: &str| { + client + .post(format!("{origin}/webhooks/polymorfa")) + .header("x-webhook-signature", signature) + .body(fixture.body.clone()) + }; + assert_eq!( + webhook("bad").send().await.unwrap().status(), + StatusCode::BAD_REQUEST + ); + for _ in 0..2 { + assert_eq!( + webhook(&fixture.signature).send().await.unwrap().status(), + StatusCode::OK + ); + } + let snapshot = || { + client + .get(format!("{origin}/api/polymorfa/conversation")) + .header("cookie", &cookie) + }; + let first: serde_json::Value = snapshot().send().await.unwrap().json().await.unwrap(); + assert_eq!(first["messages"][0]["text"], "Native send"); + assert_eq!(first["messages"][1]["text"], "Independent reply"); + assert_eq!(first["receivedEvents"], 1); + assert_eq!( + first, + snapshot() + .send() + .await + .unwrap() + .json::() + .await + .unwrap() + ); + client + .post(format!("{origin}/api/logout")) + .header("origin", &origin) + .header("cookie", &cookie) + .send() + .await + .unwrap(); + assert_eq!( + snapshot().send().await.unwrap().status(), + StatusCode::UNAUTHORIZED + ); + assert_eq!( + send(json!({"text":"Native send"})) + .send() + .await + .unwrap() + .status(), + StatusCode::UNAUTHORIZED + ); + server.abort(); + api.abort(); +} diff --git a/examples/typescript-contracts/campaign-and-delivery.ts b/examples/typescript-contracts/campaign-and-delivery.ts new file mode 100644 index 00000000..2a65556d --- /dev/null +++ b/examples/typescript-contracts/campaign-and-delivery.ts @@ -0,0 +1,40 @@ +import type { + Client, + MessagingClient, + CampaignVariableMapping, + HistorySyncPayload, + SignalCustodyMode, +} from "@polymorfa/sdk"; + +/** Source example; API admission and matching package availability are required. */ +export async function createDraft(organization: Client, projectId: string) { + const variableMapping: CampaignVariableMapping = { + first_name: { source: "name", fallback: "Friend" }, + }; + return organization.campaigns.create({ + projectId, + name: "Customer-authored audience", + variableMapping, + trackedLinks: [{ key: "details", url: "https://example.com/details" }], + }); +} + +/** Consent and custody are distinct. Supplementary links cannot change mode. */ +export function initialInvitation( + messaging: MessagingClient, + signalMode: SignalCustodyMode, +) { + return messaging.quickLinks.create({ + signalMode, + configuration: { historySync: { consent: "ask", delivery: "descriptor" } }, + }); +} + +/** The public notification identity is not the event envelope UUID. */ +export function notificationIdentity( + payload: HistorySyncPayload, +): string | undefined { + if ("delivery" in payload) return payload.whatsapp_ids.linked_devices; + if (!("kind" in payload)) return payload.whatsapp_ids.linked_devices; + return undefined; +} diff --git a/package-lock.json b/package-lock.json index f507e08e..28a23e5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,16 +17,34 @@ "packages/store", "packages/nextjs", "packages/devtools", - "examples/calls-example" + "examples/calls-example", + "packages/storage" ], "devDependencies": { + "@aws-sdk/client-dynamodb": "^3.1149.0", + "@aws-sdk/util-dynamodb": "^3.996.9", + "@azure/cosmos": "^4.10.1", "@eslint/js": "^9.39.5", + "@google-cloud/firestore": "^9.3.1", + "@libsql/client": "^0.18.0", "@types/node": "^24.0.0", + "@types/pg": "^8.23.1", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "convex": "^1.46.0", + "convex-test": "^0.0.61", + "drizzle-orm": "^0.45.4", + "esbuild": "^0.25.12", "eslint": "^9.39.5", "fake-indexeddb": "^6.2.5", "happy-dom": "^20.11.2", + "miniflare": "^4.20260730.0", + "mongodb": "^7.7.0", + "mysql2": "^3.24.5", + "pg": "^8.23.1", + "postgres": "^3.4.9", "prettier": "^3.9.6", "react": "^19.2.8", "react-dom": "^19.2.8", @@ -54,410 +72,842 @@ "node": ">=22" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], + "node_modules/@aws-sdk/checksums": { + "version": "3.1001.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.2.tgz", + "integrity": "sha512-gKRj5IXZ/O0ehk2rwfb55YygdNKNhK5N1l/Ztg/w06tk21AJtCPuyErETk2RS/6XOmpBkcZY6CN3GlQ2fWTh1g==", "dev": true, - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.1149.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.1149.0.tgz", + "integrity": "sha512-4rQKa/Y9C4p1QFGPni2OjDiLLyIZi2niGiWUktzxZl5dxMZycz7bKjzt6c7kQ5CA0KzCcz8hu/huyXneYTBEkg==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/credential-provider-node": "^3.972.85", + "@aws-sdk/dynamodb-codec": "^3.973.47", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.32", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-sdk/client-s3": { + "version": "3.1149.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1149.0.tgz", + "integrity": "sha512-wEccG+3/ap7iIBO4DbWwqNHh2u5Qm5OCQJzJaAvtVFIoZbp0qia8HIXv4rpnNmJJEX8205mlq67F9QJ0nKlcxg==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@aws-sdk/checksums": "^3.1001.2", + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/credential-provider-node": "^3.972.85", + "@aws-sdk/middleware-sdk-s3": "^3.972.78", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], + "node_modules/@aws-sdk/core": { + "version": "3.978.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.2.tgz", + "integrity": "sha512-ViOBLuGSSyPAmkKGJ6KJXtVN7MOEp3rnI7uH+kWxnHccvo8C9cvunEAnN5/oaZH1laQfsTBw7IOnmNkWlFZl2A==", + "devOptional": true, + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@aws-sdk/xml-builder": "^3.972.41", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.35.2", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.73", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.73.tgz", + "integrity": "sha512-PxlFZiF5IPUsPnuVSqjM9jlmRwZhL0xx40ccWfZiLQS8TRnHQpegpK1uAidatWuEqbJtbbY97A2Sezikz2AMUw==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.75", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.75.tgz", + "integrity": "sha512-Q/uIDy8eAVN7JgSKDg16F/f+hmNJj/ZyLJN8AkW2ofVdDgJVh8fO/A26iZ7YsvM2/k76hsf/u2lLAtyXsK8Ziw==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.18.tgz", + "integrity": "sha512-KoyjZ9vdjuxEQ8obAH06xKP8H0WKqGhwUZLMqAOiS7G0xgUAht4X8f0ASLC/vhNLCWGblZOD3VQHWyTROGJHLg==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/credential-provider-env": "^3.972.73", + "@aws-sdk/credential-provider-http": "^3.972.75", + "@aws-sdk/credential-provider-login": "^3.972.80", + "@aws-sdk/credential-provider-process": "^3.972.73", + "@aws-sdk/credential-provider-sso": "^3.973.17", + "@aws-sdk/credential-provider-web-identity": "^3.972.79", + "@aws-sdk/nested-clients": "^3.997.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.80.tgz", + "integrity": "sha512-xF9A0gaO7k+1dYPFoo69OQatA9Ov0QuDZfqE8H0FLyIoOph+mlEC+I1Qo9UULupGuDfSYh+kVpfcDSxSYf/2fg==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/nested-clients": "^3.997.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.85", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.85.tgz", + "integrity": "sha512-CrFcML/xImiSlquH4W3V6Npz3aSkPXHDVciAU1JOBN0vqjmkcz9H9up2sCHQqN76J/WrT+TJ580L8h2joksQrg==", + "devOptional": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.73", + "@aws-sdk/credential-provider-http": "^3.972.75", + "@aws-sdk/credential-provider-ini": "^3.973.18", + "@aws-sdk/credential-provider-process": "^3.972.73", + "@aws-sdk/credential-provider-sso": "^3.973.17", + "@aws-sdk/credential-provider-web-identity": "^3.972.79", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.73", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.73.tgz", + "integrity": "sha512-tyNnT4aVuOEXmKKKZPExBi828vRd9heshobBsKF0bov5EORHSJc1AGsHOMRP57sDt+dIuVASUOywaixiX26t4g==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.17.tgz", + "integrity": "sha512-gxKTWaUk2gSbBOH4m3NPRfEKcGXeDAOgHr7nNG5JktR1xZLo0R+DpMjbcleoHbB0uJREOG7hjIRE635IuBWsRQ==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/nested-clients": "^3.997.47", + "@aws-sdk/token-providers": "3.1149.0", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.79", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.79.tgz", + "integrity": "sha512-/jn0Gs/qm6fs6nTIA5yncHayybz42SATZ3rNxCEBYPJ9v69PLrDRgk07E9xTT2gpRcNmDXXbi27ZbuU06N0Z6Q==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/nested-clients": "^3.997.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/dynamodb-codec": { + "version": "3.973.47", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.973.47.tgz", + "integrity": "sha512-PFGGjcLRw63trbMZB5sQ7QX703pM1hMk+3e2dF12fppwqPBddjOSTQDRex6nVHUgzPs/e5JQETWK3SXEio7gqg==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.11.tgz", + "integrity": "sha512-8q1ICxcDjHId3bBryuu/j+1L9y5/3uQnwzLDt5j2ElcjZSoWmFtymdJy7OjLrluSMe0Z4mq5bcH4fxBXvlEHfw==", + "devOptional": true, + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.972.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.32.tgz", + "integrity": "sha512-SshKt+B6n8Nn7EHs8aRs1Ye+GJmgeX0lq1lIIV7R5tRZdTGBMzO+nXqvWMTxfN5UmNYYfSBRJbbwbl0xevusFg==", + "devOptional": true, + "dependencies": { + "@aws-sdk/endpoint-cache": "^3.972.11", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.78", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.78.tgz", + "integrity": "sha512-W1kXIqMBkfrpVTbOOUSt63D2mEM4882TLqGWXjkZ1OcJ2ggSEWzgyjlyVDIHu/goGJSREd5s/KQ9wAgVZ0djeQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.47", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.47.tgz", + "integrity": "sha512-bovCKvDsLocNTzaj6VrrMVJ2BGROqO0JlwcGseO67a+I9Bo+08nUJhSQfZUIx4+A4a7I4101GEJd/AYm7dQ68w==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.48.tgz", + "integrity": "sha512-cFDXfEbXaGwbnpvR/DyqfxfjxpjXVRVthQLgkwvcfDS5u3+8G9WSj1X1Ag/pTRg/QOeOno1Tq3ycEdrKsCUcOA==", + "devOptional": true, + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1149.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1149.0.tgz", + "integrity": "sha512-CAwYyA8eVk1w6h8YgLvkjIOakWPwURaUFrP7BRVgkxTFUM+O87thyKsGKJ6rVPbP8eoEV10cBWQ5YfaYl3qSVQ==", + "devOptional": true, + "dependencies": { + "@aws-sdk/core": "^3.978.2", + "@aws-sdk/nested-clients": "^3.997.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz", + "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==", + "devOptional": true, + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-dynamodb": { + "version": "3.996.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.996.9.tgz", + "integrity": "sha512-16x2tRvl7OYpZ0W/DdFJieFriD13+RvuRBDbe5sj/tCEfK86HSGd7I2s5j0ivz8p6KWGkS+5wKRO9OliJkjUOQ==", + "devOptional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.1111.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "devOptional": true, + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "devOptional": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.9.0.tgz", + "integrity": "sha512-6933vNLqh06RR7rumnrq3UZIeBtRLeBHDPMrieDPIljiCaMMwt0nRw++nd3TOxiohtasNtm29rC+b/9ZVKCOqg==", + "devOptional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.24.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "devOptional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "devOptional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "devOptional": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.7.0.tgz", + "integrity": "sha512-7GEAoIsaoBr6KELNRb8nypowCqvk8dnCHFCYg4XD4lOQGY2GqjQg5IhkRjyBFRO18CGSMq05PaNqSOE9GQro3g==", + "devOptional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "devOptional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "devOptional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "devOptional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/cosmos": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.10.1.tgz", + "integrity": "sha512-rTL/F4Uld6Te3UqPt4jUewLlf537uvxu52fVMntcChJRIyc7Pf2K8B6U4TPYmwsLlrjvAUC9lJnHGfX0jWNGPQ==", + "devOptional": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-keys": "^4.9.0", + "@azure/logger": "^1.1.4", + "fast-json-stable-stringify": "^2.1.0", + "priorityqueuejs": "^2.0.0", + "semaphore": "^1.1.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.1.0.tgz", + "integrity": "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==", + "devOptional": true, + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.2.tgz", + "integrity": "sha512-VmUSLbXRAbSzDD8grXHGPaknYs0SKr3yuf6U+d4XMpX4XuVYskNqbTTwXce0zR1LyxfTZm9rWEBcvs3vdYwCmQ==", + "devOptional": true, + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.1.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "devOptional": true, + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@bufbuild/buf": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.73.0.tgz", + "integrity": "sha512-W3whb4P1y3Hs9bqwCu9++W1v1ofHZ3dBUCe6JSZq0Ff5OK5UKKZuMN4iunPbgdyXY4+4ujEYf2ESrN/6MbcFSQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "buf": "bin/buf", + "protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking", + "protoc-gen-buf-lint": "bin/protoc-gen-buf-lint" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@bufbuild/buf-darwin-arm64": "1.73.0", + "@bufbuild/buf-darwin-x64": "1.73.0", + "@bufbuild/buf-linux-aarch64": "1.73.0", + "@bufbuild/buf-linux-armv7": "1.73.0", + "@bufbuild/buf-linux-x64": "1.73.0", + "@bufbuild/buf-win32-arm64": "1.73.0", + "@bufbuild/buf-win32-x64": "1.73.0" + } + }, + "node_modules/@bufbuild/buf-darwin-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.73.0.tgz", + "integrity": "sha512-cDd3y376ecClhyytNLlY3AtsZqsqXvEkQjMvrahrv/xUWWhEIX3efD8y0uufXDe8Yuk3sEmj7J1Xlx73Y46SkA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-darwin-x64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.73.0.tgz", + "integrity": "sha512-VTBTCwaqT7otsMGXlk3MRu6x5hp+YH9PaLKcyWAa8wEt0NnMzbXdkE8GiLgI8Qkl7S3+pC9YSTCE/I+O87Fvrw==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "node_modules/@bufbuild/buf-linux-aarch64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.73.0.tgz", + "integrity": "sha512-HjoQ3wbLiUv5N5XLM3EAR2FY51Yn6t5MEXLPnS/Hx0KDkK/5eyIBZZ6toHBLjczAr1RTA1LagF38+lglZI+kvA==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "node_modules/@bufbuild/buf-linux-armv7": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.73.0.tgz", + "integrity": "sha512-NsZuVg8hXfGcR96Is14ArKxZg6pdJb6kb63bx2g7q8Cjo+gRqZMBFKGvF/fnQMxTrRQAm6hzeF4a2vV4W3lszg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-linux-x64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.73.0.tgz", + "integrity": "sha512-dMe6lBQrlHTU7DzqzaR7Udky7R78EDzzxtYZQdjkZun9jKdjGR4fLbPWEPyY4rLGVyk6UUnrtkf5qoIK2SC6qw==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "node_modules/@bufbuild/buf-win32-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.73.0.tgz", + "integrity": "sha512-UmubCflc8zJ48bVW0WeGeVTkNuumJP//wCWKUmeTXT1iNlEqkpaXy6y7o8YJKHGWLfX0/1UV3oay9ti0EyojMw==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "node_modules/@bufbuild/buf-win32-x64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.73.0.tgz", + "integrity": "sha512-/Buo10rXzqNLnFDh3BpzGdcrx6OalcU8o8gUyObCFZA5wKcUvBVNDhhpMmoB6YeiOPpPn47jHgW4bSSlypaaOQ==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "node_modules/@bufbuild/protobuf": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.16.0.tgz", + "integrity": "sha512-FWa0sPlqYGJgpTs6OxBRcL/AW4JT0OIO+W1ajerluoTVh8CV0B7XM1qsNCRgnkSCkRCgOICKHRQlY17jrsm0+Q==", + "dev": true + }, + "node_modules/@bufbuild/protoc-gen-es": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-2.16.0.tgz", + "integrity": "sha512-LbPrEaGd7bOxKbJHllTR5439+PZjqd6fM7xyDZkDVtZ+Efzr7TMdMqShG9Qkf/nYVyiNS/SK5r9JhRaBTFkfZw==", + "dev": true, + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@bufbuild/protoplugin": "2.16.0" + }, + "bin": { + "protoc-gen-es": "bin/protoc-gen-es" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@bufbuild/protobuf": "2.16.0" + }, + "peerDependenciesMeta": { + "@bufbuild/protobuf": { + "optional": true + } + } + }, + "node_modules/@bufbuild/protoplugin": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.16.0.tgz", + "integrity": "sha512-3FdYJys34VhFnBQsjiV9wurBkX/DBACQAkAk+nCKdO1p8ROVNi7gmaRAcba+4qq8hQq8rYVukuutwvGCvd4kJA==", + "dev": true, + "dependencies": { + "@bufbuild/protobuf": "2.16.0", + "@typescript/vfs": "^1.6.2", + "typescript": "5.4.5" + } + }, + "node_modules/@bufbuild/protoplugin/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260730.1.tgz", + "integrity": "sha512-+MBHmPaiTe2KajryW0T24rZvWFxb41hD3d8anNzQqHzft6vSEb18+sp0znSwxgij7ApPhSM1+vhkNg4f3YMguA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "optional": true, "os": [ - "openharmony" + "darwin" ], "engines": { - "node": ">=18" + "node": ">=16" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260730.1.tgz", + "integrity": "sha512-SBHKntPkKvNPgaCrTe99xC1CAl8ygJDzlYfK0LbuJ1muKadIw35WnhO0wu894fKBtllsVQdNzDLee+cm0ppLSQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, "optional": true, "os": [ - "sunos" + "darwin" ], "engines": { - "node": ">=18" + "node": ">=16" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260730.1.tgz", + "integrity": "sha512-ouyPOSMbiKPeSwUJUvxtMcxGAXs2J4aPE4T5ABIYX5ClcQx5j5bbHTmnqOQEY8sAuLTPjH7dY+iB6UI5ISlwwA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": ">=16" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260730.1.tgz", + "integrity": "sha512-YQ+Mi78U3TPdgBPtwq+Sm6rJU+Ihl2y0pjYtuuKkdmUbYzL7oLR6Xqq9wljhasnuCFICssDJaqhMep5WizYoEQ==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": ">=16" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260730.1.tgz", + "integrity": "sha512-27fAN+vUECW1oYVc1KOcHYpkL8COM2Uxtxql7TL595kxbjoqS5yckw7NLz7bTf2pALFCZWjqXDjZGJ/xbG4ZKQ==", "cpu": [ "x64" ], @@ -467,270 +917,291 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=16" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=12" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@eslint/core": "^0.17.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", - "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.3.0", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/@eslint/js": { - "version": "9.39.5", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", - "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "@humanfs/types": "^0.15.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], "dev": true, - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true - }, - "node_modules/@napi-rs/lzma-linux-x64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", - "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -740,959 +1211,3237 @@ "linux" ], "engines": { - "node": "^22.20 || ^24.12 || >=25" + "node": ">=18" } }, - "node_modules/@polymorfa/browser": { - "resolved": "packages/browser", - "link": true - }, - "node_modules/@polymorfa/calls": { - "resolved": "packages/calls", - "link": true - }, - "node_modules/@polymorfa/devtools": { - "resolved": "packages/devtools", - "link": true - }, - "node_modules/@polymorfa/elements": { - "resolved": "packages/elements", - "link": true - }, - "node_modules/@polymorfa/example-calls": { - "resolved": "examples/calls-example", - "link": true - }, - "node_modules/@polymorfa/nextjs": { - "resolved": "packages/nextjs", - "link": true - }, - "node_modules/@polymorfa/react": { - "resolved": "packages/react", - "link": true - }, - "node_modules/@polymorfa/sdk": { - "resolved": "packages/typescript", - "link": true - }, - "node_modules/@polymorfa/store": { - "resolved": "packages/store", - "link": true - }, - "node_modules/@polymorfa/ui": { - "resolved": "packages/ui", - "link": true - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", - "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ - "arm" + "arm64" ], "dev": true, "optional": true, "os": [ - "android" - ] + "netbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", - "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ - "arm64" + "x64" ], "dev": true, "optional": true, "os": [ - "android" - ] + "netbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", - "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ - "darwin" - ] + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", - "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ - "darwin" - ] + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", - "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ - "freebsd" - ] + "openharmony" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", - "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", - "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", - "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", - "cpu": [ - "arm" + "sunos" ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", - "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", - "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", - "cpu": [ - "arm64" + "win32" ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", - "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ - "loong64" + "ia32" ], "dev": true, "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", - "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", - "cpu": [ - "loong64" + "win32" ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", - "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", - "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", - "cpu": [ - "ppc64" + "win32" ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", - "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", - "cpu": [ - "riscv64" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", - "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", - "cpu": [ - "riscv64" - ], + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", - "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", - "cpu": [ - "s390x" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", - "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", - "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", - "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, - "optional": true, - "os": [ - "openbsd" - ] + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", - "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", - "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", - "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", - "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", - "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "dependencies": { - "undici-types": "~7.18.0" + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@types/react": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", - "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "dependencies": { - "csstype": "^3.2.2" + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@types/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, - "peerDependencies": { - "@types/react": "^19.2.0" + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@types/whatwg-mimetype": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", - "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "dependencies": { - "@types/node": "*" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", - "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.67.0", - "@typescript-eslint/type-utils": "8.67.0", - "@typescript-eslint/utils": "8.67.0", - "@typescript-eslint/visitor-keys": "8.67.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" + "brace-expansion": "^1.1.7" }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.67.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "url": "https://eslint.org/donate" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "engines": { - "node": ">= 4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", - "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.67.0", - "@typescript-eslint/types": "8.67.0", - "@typescript-eslint/typescript-estree": "8.67.0", - "@typescript-eslint/visitor-keys": "8.67.0", - "debug": "^4.4.3" + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", - "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", - "dev": true, + "node_modules/@google-cloud/firestore": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-9.3.1.tgz", + "integrity": "sha512-ofGTiSpfXrHrrS3KaDOmsupSzCHr4WVl1+s6Ke0F8rPBRyb/OuZSC8u1NjCKtt/fO7A1GvqASQGhdAwVsnGpQA==", + "devOptional": true, "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.67.0", - "@typescript-eslint/types": "^8.67.0", - "debug": "^4.4.3" + "@google-cloud/firestore-api": "^0.2.0", + "@opentelemetry/api": "^1.9.0", + "fast-deep-equal": "^3.1.3", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^6.0.1", + "protobufjs": "^7.5.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=22" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", - "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", - "dev": true, + "node_modules/@google-cloud/firestore-api": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore-api/-/firestore-api-0.2.0.tgz", + "integrity": "sha512-wfHJPZmLqzpy6FN6U+7HkkP/AKhGsbHSpEy/qL2TyI3/pKRAvpamQxZ2qvps5hMRgAjfuHCim+oeJj9RveCaqg==", + "devOptional": true, "dependencies": { - "@typescript-eslint/types": "8.67.0", - "@typescript-eslint/visitor-keys": "8.67.0" + "google-gax": "^5.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=18" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", - "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node_modules/@google-cloud/firestore-api/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "devOptional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">=18" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", - "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", - "dev": true, + "node_modules/@google-cloud/firestore-api/node_modules/gcp-metadata": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.4.tgz", + "integrity": "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw==", + "devOptional": true, "dependencies": { - "@typescript-eslint/types": "8.67.0", - "@typescript-eslint/typescript-estree": "8.67.0", - "@typescript-eslint/utils": "8.67.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" + "gaxios": "7.1.3", + "google-logging-utils": "1.1.3", + "json-bigint": "^1.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "devOptional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-gax": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.8.tgz", + "integrity": "sha512-M4vpZcXQIC1gqIVGQ7eaU3jXQA6zecStyTXu514TYfThlgSurYJOxHZo9fzU6hAgwPWvuEynAVHWyaIk80VeEA==", + "devOptional": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "10.5.0", + "google-logging-utils": "1.1.3", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "3.0.4", + "protobufjs": "^7.5.4", + "retry-request": "^8.0.2", + "rimraf": "^5.0.1" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">=18" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", - "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", - "dev": true, + "node_modules/@google-cloud/firestore-api/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "devOptional": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=14" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", - "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", - "dev": true, + "node_modules/@google-cloud/firestore-api/node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "devOptional": true, "dependencies": { - "@typescript-eslint/project-service": "8.67.0", - "@typescript-eslint/tsconfig-utils": "8.67.0", - "@typescript-eslint/types": "8.67.0", - "@typescript-eslint/visitor-keys": "8.67.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" + "protobufjs": "^7.4.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/retry-request": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.4.tgz", + "integrity": "sha512-pI6/7eabUYkZxamkOq0g0uMxKLLGnjzhefY+vL8bVXag5rto4OU2YBTPytWLuHH7aEKD6fn7kJycQfid0Mwnkw==", + "devOptional": true, + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">=18" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", - "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", - "dev": true, + "node_modules/@grpc/grpc-js": { + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.6.tgz", + "integrity": "sha512-SgC/B3/VZ8lTyOZ4Cwk6f1skWhmkNAaVfiX90qd5u1rRXGKk1//8FTUEN9GvOUq7rfWIqAYkA3CoyumYVpOS6g==", + "devOptional": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.67.0", - "@typescript-eslint/types": "8.67.0", - "@typescript-eslint/typescript-estree": "8.67.0" + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "devOptional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">=6" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", - "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.67.0", - "eslint-visitor-keys": "^5.0.0" + "@humanfs/types": "^0.15.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=18.18.0" } }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=18.18.0" } }, - "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, - "dependencies": { - "@vitest/spy": "3.2.7", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "engines": { + "node": ">=18.18.0" } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "dependencies": { - "tinyrainbow": "^2.0.0" + "engines": { + "node": ">=12.22" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "engines": { + "node": ">=18.18" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "dev": true, - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, - "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "tinyspy": "^4.0.3" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, - "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://opencollective.com/libvips" } }, - "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], "dev": true, - "bin": { - "acorn": "bin/acorn" + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.4.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "dev": true, + "optional": true, "dependencies": { - "color-convert": "^2.0.1" + "@emnapi/runtime": "^1.11.3" }, "engines": { - "node": ">=8" + "node": ">=20.9.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://opencollective.com/libvips" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "devOptional": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "devOptional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "devOptional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "devOptional": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "devOptional": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "devOptional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@libsql/client": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.18.0.tgz", + "integrity": "sha512-zMCCo58vv7w27j7+cnt/GW+X6/Rih6cHZ86PKi0AxCMOqMfrVgG9OZ5lW2bDMJhSeOjsv2szH2nJF3A9l36UmA==", + "dev": true, + "dependencies": { + "@libsql/core": "^0.18.0", + "@libsql/hrana-client": "^0.10.0", + "js-base64": "^3.7.5", + "libsql": "^0.5.28", + "promise-limit": "^2.7.0" + } + }, + "node_modules/@libsql/core": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.18.0.tgz", + "integrity": "sha512-N8RR2CIpcgtbKZnXs2KVpw2lJQfinUj3I56e7qRDRpK18Hty0Rr8nr00VmFb4R/DsqOanPB/TaP3+QAQLZeXMg==", + "dev": true, + "dependencies": { + "js-base64": "^3.7.5" + } + }, + "node_modules/@libsql/darwin-arm64": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz", + "integrity": "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@libsql/darwin-x64": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz", + "integrity": "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@libsql/hrana-client": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.10.0.tgz", + "integrity": "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw==", + "dev": true, + "dependencies": { + "@libsql/isomorphic-ws": "^0.1.5", + "js-base64": "^3.7.5" + } + }, + "node_modules/@libsql/isomorphic-ws": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz", + "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==", + "dev": true, + "dependencies": { + "@types/ws": "^8.5.4", + "ws": "^8.13.0" + } + }, + "node_modules/@libsql/linux-arm-gnueabihf": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.29.tgz", + "integrity": "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm-musleabihf": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.29.tgz", + "integrity": "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm64-gnu": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz", + "integrity": "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm64-musl": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz", + "integrity": "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-x64-gnu": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz", + "integrity": "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-x64-musl": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz", + "integrity": "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/win32-x64-msvc": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.29.tgz", + "integrity": "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.6.0.tgz", + "integrity": "sha512-19jGXfpctLDJUELLUZGJ5BllcLT7rHdF+gTTaN2jzqu0bGkUhpdcYtgK2Vux+g3zuBAa0TM/W666QMIjYhYujw==", + "devOptional": true, + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@neon-rs/load": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", + "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==", + "dev": true + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "devOptional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polymorfa/browser": { + "resolved": "packages/browser", + "link": true + }, + "node_modules/@polymorfa/calls": { + "resolved": "packages/calls", + "link": true + }, + "node_modules/@polymorfa/devtools": { + "resolved": "packages/devtools", + "link": true + }, + "node_modules/@polymorfa/elements": { + "resolved": "packages/elements", + "link": true + }, + "node_modules/@polymorfa/example-calls": { + "resolved": "examples/calls-example", + "link": true + }, + "node_modules/@polymorfa/nextjs": { + "resolved": "packages/nextjs", + "link": true + }, + "node_modules/@polymorfa/react": { + "resolved": "packages/react", + "link": true + }, + "node_modules/@polymorfa/sdk": { + "resolved": "packages/typescript", + "link": true + }, + "node_modules/@polymorfa/storage": { + "resolved": "packages/storage", + "link": true + }, + "node_modules/@polymorfa/store": { + "resolved": "packages/store", + "link": true + }, + "node_modules/@polymorfa/ui": { + "resolved": "packages/ui", + "link": true + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/dumper/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "devOptional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "devOptional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "devOptional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "devOptional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "devOptional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "devOptional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "devOptional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "devOptional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "devOptional": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@smithy/core": { + "version": "3.35.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.2.tgz", + "integrity": "sha512-ejTukBuP/Pg3t9N8Ux+0zKsXeHWm7p6W3HbXFOyltKasSxZwCFmvTut7kBdZSCF3m9y1bXXyVu6p2xuMFcsTyQ==", + "devOptional": true, + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "devOptional": true, + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "devOptional": true, + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "devOptional": true, + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "devOptional": true, + "dependencies": { + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "devOptional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.24", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz", + "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "devOptional": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "devOptional": true + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "devOptional": true, + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.67.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript/vfs": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.5.tgz", + "integrity": "sha512-gobLHQLmvQGJInFk31NvgMd1uYzRXsd8qgnOssyLD055R2QPnw1ubRbBQ4+T+5qC7IQVuYXdvNDE2G7tMDVHgw==", + "dev": true, + "dependencies": { + "debug": "^4.4.3" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz", + "integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==", + "devOptional": true, + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "devOptional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "dev": true, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "devOptional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/bson": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.3.3.tgz", + "integrity": "sha512-oz3LwE3qGEhTCSyhzQRLY3Bj1NLOsjFMf6tcmY5J8M+3R7EVcmoBfSHWvjUK3yXCLDUsGpIuB3aQL4nDZ1EV/g==", + "devOptional": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "devOptional": true + }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convex": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.46.0.tgz", + "integrity": "sha512-dhBwDQTZDOhi5SUGAX17EWNehCdNq6/UFrSL0eLZuGfMkdsQbCVWVRZCzbZ5CF+/pgvoGxtApbEoTSipqL4JTw==", + "devOptional": true, + "dependencies": { + "esbuild": "0.27.0", + "prettier": "^3.0.0", + "ws": "8.21.0" + }, + "bin": { + "convex": "bin/main.js" + }, + "engines": { + "node": ">=20.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@auth0/auth0-react": "^2.0.1", + "@clerk/clerk-react": "^4.12.8 || ^5.0.0", + "@clerk/react": "^6.4.3", + "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@auth0/auth0-react": { + "optional": true + }, + "@clerk/clerk-react": { + "optional": true + }, + "@clerk/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/convex-test": { + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/convex-test/-/convex-test-0.0.61.tgz", + "integrity": "sha512-uYKCFk9+iu8jJB7TW3BHm3EAM6yPoXERoLHGiomhgUwuTLdSUjOk9SYnnnDc1UA1MQrMPYfUFSp+0+GWyFREng==", + "dev": true, + "peerDependencies": { + "convex": "^1.43.0" + } + }, + "node_modules/convex/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/convex/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/convex/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" } }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "node_modules/convex/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "balanced-match": "^4.0.2" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": "20 || >=22" + "node": ">=18" } }, - "node_modules/buffer-image-size": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", - "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "node_modules/convex/node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@types/node": "*" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=4.0" + "node": ">=18" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/convex/node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/convex/node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], "dev": true, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "node_modules/convex/node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=18" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "node_modules/convex/node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "devOptional": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=10" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/convex/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "devOptional": true, "engines": { - "node": ">= 16" + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "devOptional": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1708,11 +4457,20 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "devOptional": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "devOptional": true, "dependencies": { "ms": "^2.1.3" }, @@ -1725,20 +4483,200 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "engines": { - "node": ">=6" + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.4", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.4.tgz", + "integrity": "sha512-NiM1YZpJ2UxyeE67DpDDZ+Tsw4YLX3trSlkNT7Tm9eKoUNb1LvaAaQzXgYQsWs+AQ9TUmmwMmBOCXuqqxUWKKQ==", + "devOptional": true, + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@netlify/db": ">=0.4.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@netlify/db": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "devOptional": true + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "devOptional": true, + "dependencies": { + "once": "^1.4.0" + } }, "node_modules/entities": { "version": "7.0.1", @@ -1752,6 +4690,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -1799,6 +4746,15 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2027,6 +4983,12 @@ "node": ">=12.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "devOptional": true + }, "node_modules/fake-indexeddb": { "version": "6.2.5", "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", @@ -2040,13 +5002,13 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "devOptional": true }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "devOptional": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -2071,6 +5033,29 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2118,6 +5103,34 @@ "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "devOptional": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2132,6 +5145,92 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "devOptional": true + }, + "node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "devOptional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/google-logging-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.2.0.tgz", + "integrity": "sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "devOptional": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2144,6 +5243,36 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -2156,6 +5285,81 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.2.0.tgz", + "integrity": "sha512-R9/Dwc3swSur04RnizQ6EBUmmcno2cC5cAKtrG/C+pY+W7c5l6rSnFWgMkR7NSYj1n6XWmb/KhcjxGslWm4yIA==", + "devOptional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "^9.0.0", + "google-logging-utils": "^2.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.1.0.tgz", + "integrity": "sha512-hV4Abmis9XY9ch/HK8t4oGy/Ho4WNzd6QAj/7PHKF5Ae25LUDviZET0s7DD3i+mG1QpGCqnDji/R/jBiuux4jw==", + "devOptional": true, + "dependencies": { + "gaxios": "^7.1.3", + "google-logging-utils": "^2.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-gax": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-6.12.1.tgz", + "integrity": "sha512-U/OUrt4+aUiELB8sgaPJAdVfPzFOPjo1ngXVOwbR1NcCHV7ZcPOW0Ecwdbp8J86NmxBwLMJUOj8AgRSOZm5iJQ==", + "devOptional": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "@opentelemetry/api": "^1.9.0", + "duplexify": "^4.1.3", + "google-auth-library": "^11.0.0", + "google-logging-utils": "^2.0.0", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^4.0.0", + "protobufjs": "^7.5.4", + "retry-request": "^9.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-logging-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.0.1.tgz", + "integrity": "sha512-HMhaQghlOTvbcb3c4T5jmmOMtG3JUF1iOQMezaJXL86CDS+Tm2vHd0IeLFRAx3+ewd+bo9E1HFHoy17X5aJa9A==", + "devOptional": true, + "engines": { + "node": ">=22" + } + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "devOptional": true, + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/happy-dom": { "version": "20.11.2", "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.2.tgz", @@ -2183,6 +5387,48 @@ "node": ">=8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "devOptional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "devOptional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2217,6 +5463,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2226,6 +5478,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2238,10 +5499,37 @@ "node": ">=0.10.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "devOptional": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-base64": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.9.4.tgz", + "integrity": "sha512-PtOMXpEGuP0RRiRXsjzHzl44dMHxSu2CPvAhinupR1tBa88me+1DPqsobl7eupn5UukjLkln1ZnAOAOXOrYG0Q==", "dev": true }, "node_modules/js-tokens": { @@ -2272,6 +5560,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "devOptional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2279,10 +5576,11 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -2290,6 +5588,27 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2299,6 +5618,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2312,6 +5640,38 @@ "node": ">= 0.8.0" } }, + "node_modules/libsql": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.29.tgz", + "integrity": "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==", + "cpu": [ + "x64", + "arm64", + "wasm32", + "arm" + ], + "dev": true, + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "@neon-rs/load": "^0.0.4", + "detect-libc": "2.0.2" + }, + "optionalDependencies": { + "@libsql/darwin-arm64": "0.5.29", + "@libsql/darwin-x64": "0.5.29", + "@libsql/linux-arm-gnueabihf": "0.5.29", + "@libsql/linux-arm-musleabihf": "0.5.29", + "@libsql/linux-arm64-gnu": "0.5.29", + "@libsql/linux-arm64-musl": "0.5.29", + "@libsql/linux-x64-gnu": "0.5.29", + "@libsql/linux-x64-musl": "0.5.29", + "@libsql/win32-x64-msvc": "0.5.29" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2327,18 +5687,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "devOptional": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "devOptional": true + }, + "node_modules/lru.min": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz", + "integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==", + "dev": true, + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2348,6 +5741,53 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "devOptional": true + }, + "node_modules/miniflare": { + "version": "4.20260730.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260730.0.tgz", + "integrity": "sha512-1Z9SB9r/o//80UA02Re3QhtcecSHAyAjf5EcKBfQVlQrCg7Miy79hl2PvtkwFLIaJ5rcrOPdDcRr577okwZPsg==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.2", + "undici": "7.28.0", + "workerd": "1.20260730.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/miniflare/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -2363,11 +5803,121 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "devOptional": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "devOptional": true, + "dependencies": { + "obliterator": "^1.6.1" + } + }, + "node_modules/mongodb": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.7.0.tgz", + "integrity": "sha512-XG1xXEqyouAV8GCb3PuRaujofeXKpAJAPvXYWZgHl6Dz4QHSJ1nRLl0+GpqaLTwKvC0RFE9truOur34RNFY1nw==", + "devOptional": true, + "dependencies": { + "@mongodb-js/saslprep": "^1.5.0", + "bson": "^7.3.2", + "mongodb-connection-string-url": "^7.0.2" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": "^7.2.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.2.tgz", + "integrity": "sha512-ZoS07RoFqpKYQwAk59qmrx8+jJHNHU30UjlU96QktiGn1ltvDr+vCznLX5DiUBLEpMAHatHNWV1nM/74ul66kA==", + "devOptional": true, + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "devOptional": true + }, + "node_modules/mysql2": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.5.tgz", + "integrity": "sha512-X6Ujsr2QSkkLpkQGjxzpKRAPn9nu4axpR63ntBzquFVEvPOArgbUQ1sJjFKI7hnaYtiVZxa17Z7q18KSubW0IQ==", + "dev": true, + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.3", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.5.1" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "dev": true, + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } }, "node_modules/nanoid": { "version": "3.3.18", @@ -2393,6 +5943,68 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "devOptional": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "devOptional": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2440,6 +6052,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "devOptional": true + }, + "node_modules/pako": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pako/-/pako-3.0.2.tgz", + "integrity": "sha512-uBv6IT2aT1A78iU6dpNEbf6+CyhlV/6g9JlJs9kpgjFGFhruIICVRysF/W0SLzXg5+hCl+KroH7e4YUyfEmgLg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2465,11 +6099,27 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "devOptional": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -2485,6 +6135,95 @@ "node": ">= 14.16" } }, + "node_modules/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==", + "dev": true, + "dependencies": { + "pg-connection-string": "^2.14.1", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.1", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz", + "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==", + "dev": true, + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz", + "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==", + "dev": true + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "dev": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz", + "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==", + "dev": true + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2531,6 +6270,58 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", + "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", + "devOptional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2544,7 +6335,7 @@ "version": "3.9.6", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", - "dev": true, + "devOptional": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -2555,11 +6346,58 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/priorityqueuejs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-2.0.0.tgz", + "integrity": "sha512-19BMarhgpq3x4ccvVi8k2QpJZcymo/iFUcrhPd4V96kYGovOdTsWwy7fxChYi4QY+m2EnGBWSX9Buakz+tWNQQ==", + "devOptional": true + }, + "node_modules/promise-limit": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", + "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", + "dev": true + }, + "node_modules/proto3-json-serializer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-4.0.2.tgz", + "integrity": "sha512-VUQlb/J2WjOG8BKhdLwJUz+MKxSMtBGYh+TaNmRtiYVhlIzXjzeAGIx7gtc3lXlA2iURzfF2+cXaQvC2ofwzpg==", + "devOptional": true, + "dependencies": { + "protobufjs": "^7.5.4" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } @@ -2583,6 +6421,29 @@ "react": "^19.2.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2592,6 +6453,33 @@ "node": ">=4" } }, + "node_modules/retry-request": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-9.0.2.tgz", + "integrity": "sha512-S73r2yk09DSWx7K4mTZG7v5wSOrCK5okQbYLlkgpDjzVsxkUkhKrUR2VpUp69e3iA06Btn0bDXa27gxfnCqZMw==", + "devOptional": true, + "dependencies": { + "extend": "^3.0.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "devOptional": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.62.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", @@ -2637,11 +6525,46 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" }, + "node_modules/semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==", + "devOptional": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -2654,11 +6577,69 @@ "node": ">=10" } }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "dev": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sharp/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "devOptional": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2670,7 +6651,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -2681,6 +6662,18 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2690,6 +6683,39 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "devOptional": true, + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-escaper": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.2.tgz", + "integrity": "sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==", + "dev": true, + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -2702,6 +6728,84 @@ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "devOptional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "devOptional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2726,6 +6830,12 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "devOptional": true + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2738,6 +6848,21 @@ "node": ">=8" } }, + "node_modules/teeny-request": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.4.tgz", + "integrity": "sha512-R1Cg4Vu0UULeDfHL/kjABLaTW++9yD/B6n2g48y5dJ04hsEaxcfmAqbNDzNsbqAYJyIpZafjklLG9YxRu9uzOg==", + "devOptional": true, + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2793,6 +6918,18 @@ "node": ">=14.0.0" } }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "devOptional": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -2805,6 +6942,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2853,11 +6996,20 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "dev": true, + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true + "devOptional": true }, "node_modules/uri-js": { "version": "4.4.1", @@ -2868,6 +7020,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", @@ -3036,6 +7194,24 @@ } } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "devOptional": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -3045,11 +7221,24 @@ "node": ">=12" } }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "devOptional": true, + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -3085,6 +7274,67 @@ "node": ">=0.10.0" } }, + "node_modules/workerd": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260730.1.tgz", + "integrity": "sha512-zmfNIjwYSWFY5chGBOjWtH3xAE7p97FTC6vR4Ep98290ho6AeAR/NVcBD274YCLEUYzqm8yxdtZlxMybU8a3jA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260730.1", + "@cloudflare/workerd-darwin-arm64": "1.20260730.1", + "@cloudflare/workerd-linux-64": "1.20260730.1", + "@cloudflare/workerd-linux-arm64": "1.20260730.1", + "@cloudflare/workerd-windows-64": "1.20260730.1" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true + }, "node_modules/ws": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", @@ -3106,6 +7356,51 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "devOptional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -3118,6 +7413,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, "packages/browser": { "name": "@polymorfa/browser", "version": "0.1.0-dev.0", @@ -3181,6 +7499,51 @@ "react": ">=18.2.0 <20" } }, + "packages/storage": { + "name": "@polymorfa/storage", + "version": "0.1.0-dev.0", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": ">=3 <4", + "@aws-sdk/util-dynamodb": ">=3 <4", + "@azure/cosmos": ">=4 <5", + "@google-cloud/firestore": ">=7 <10", + "@polymorfa/sdk": "0.1.0-dev.0", + "convex": ">=1 <2", + "drizzle-orm": ">=0.40 <1", + "mongodb": ">=6 <8", + "postgres": ">=3 <4" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-dynamodb": { + "optional": true + }, + "@aws-sdk/util-dynamodb": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@google-cloud/firestore": { + "optional": true + }, + "convex": { + "optional": true + }, + "drizzle-orm": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "postgres": { + "optional": true + } + } + }, "packages/store": { "name": "@polymorfa/store", "version": "0.1.0-dev.0", @@ -3202,14 +7565,128 @@ "name": "@polymorfa/sdk", "version": "0.1.0-dev.0", "license": "MIT", + "dependencies": { + "pako": "3.0.2" + }, + "devDependencies": { + "@aws-sdk/client-s3": "^3.1149.0", + "@bufbuild/buf": "^1.73.0", + "@bufbuild/protobuf": "^2.16.0", + "@bufbuild/protoc-gen-es": "^2.16.0" + }, "engines": { "node": ">=20" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.0.0", + "@bufbuild/protobuf": "^2.0.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-s3": { + "optional": true + }, + "@bufbuild/protobuf": { + "optional": true + } } }, "packages/ui": { "name": "@polymorfa/ui", "version": "0.1.0-dev.0", "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } } } } diff --git a/package.json b/package.json index 9697921a..71f553bd 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "packages/store", "packages/nextjs", "packages/devtools", - "examples/calls-example" + "examples/calls-example", + "packages/storage" ], "engines": { "node": ">=20" @@ -33,21 +34,49 @@ "lint": "eslint \"packages/**/*.{ts,tsx}\" vitest.config.ts", "test": "vitest run", "typecheck": "tsc -p tsconfig.json --noEmit", - "check:functions": "node scripts/check-functions.mjs" + "check:functions": "node scripts/check-functions.mjs", + "generate:doc-snippets": "node scripts/generate-doc-snippets.mjs", + "check:doc-snippets": "node scripts/generate-doc-snippets.mjs --check", + "test:doc-snippets": "node --test scripts/doc-snippets.test.mjs", + "verify:doc-snippets": "node scripts/verify-doc-snippets.mjs", + "check:machine-credentials": "node scripts/check-machine-credentials.mjs" }, "devDependencies": { + "@aws-sdk/client-dynamodb": "^3.1149.0", + "@aws-sdk/util-dynamodb": "^3.996.9", + "@azure/cosmos": "^4.10.1", "@eslint/js": "^9.39.5", + "@google-cloud/firestore": "^9.3.1", + "@libsql/client": "^0.18.0", "@types/node": "^24.0.0", + "@types/pg": "^8.23.1", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "convex": "^1.46.0", + "convex-test": "^0.0.61", + "drizzle-orm": "^0.45.4", + "esbuild": "^0.25.12", "eslint": "^9.39.5", "fake-indexeddb": "^6.2.5", "happy-dom": "^20.11.2", + "miniflare": "^4.20260730.0", + "mongodb": "^7.7.0", + "mysql2": "^3.24.5", + "pg": "^8.23.1", + "postgres": "^3.4.9", "prettier": "^3.9.6", "react": "^19.2.8", "react-dom": "^19.2.8", "typescript": "^6.0.3", "typescript-eslint": "^8.67.0", "vitest": "^3.2.7" + }, + "overrides": { + "miniflare": { + "undici": "^7.29.1", + "sharp": "^0.35.5" + } } } diff --git a/packages/browser/README.md b/packages/browser/README.md index 5e6f9232..764ee229 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -306,3 +306,37 @@ system audio is not captured. Capture ending stops sharing. Leaving stops saved camera and display tracks, and a replacement connection does not reacquire the display. The source uses the existing single video publisher, so it cannot replace another connection's camera. Display and camera are not sent together. + +## Inline chat workspace + +See [chat components](../../docs/chat-components.md) for the inline `ChatWindow` / +`pmfa-chat-window`, observed receipts, media, presentation filters and failure-preserving +composer actions. History and sends remain owned by your authorized data source. + +`InboxConversation` accepts observed pin/archive/marked-unread state, a scope +subtitle and optional last-message receipts. Rendering this state never performs +a WhatsApp mutation. Hosts supply authorized adapters to the controlled React +list. [See the list contract](../../docs/chat-components.md#controlled-conversation-list). + +### Conversation sessions and loaded-message filters + +`createConversationSession(source, { upload, draft, sendUnavailable, conversation, composer })` +owns a +`ConversationController` and its `MessageComposerController`. `source` is the +public `ConversationDataSource` interface. The host supplies authorized reads, +subscriptions and sends; an optional upload adapter handles picked file bytes. +The factory does not discover routes, mint credentials or persist browser data. +Keep the session while switching threads to preserve its draft, reply and retry +identity, then call `dispose()` when its scope or authority closes. Disposal +cancels history reads, sends and uploads together. Pass `sendUnavailable` with a +message while the host cannot send: submitting then fails with that message, keeps +the draft and never calls `source.send`, so no failed message joins the thread. +Without an `upload` adapter, attaching a file fails instead of uploading. + +`MessageFilterController` has `setQuery`, `setType`, `setDirection`, `clear` and +`matches` methods. It filters loaded messages without fetching history or changing +conversation state. `messageMatchesFilters`, `conversationMessageKind` and +`orderSearchText` support custom views. An explicit `messageType` preserves known +media type when bytes were not retained; `contentState` prevents unavailable +content from appearing in search or default renderers. Received orders use the +public `MessageOrder` and `OrderItem` types. diff --git a/packages/browser/src/chat/composer.ts b/packages/browser/src/chat/composer.ts index 011d9881..83ee9eac 100644 --- a/packages/browser/src/chat/composer.ts +++ b/packages/browser/src/chat/composer.ts @@ -4,6 +4,7 @@ import { } from "../controller.js"; import type { ConversationController, + MediaQuality, MessageAttachment, } from "./conversation.js"; @@ -31,6 +32,8 @@ export function localAttachmentFromFile( } export interface ComposerAttachment extends LocalAttachment { + /** Requested send quality for a picture; see `setAttachmentQuality`. */ + readonly quality?: MediaQuality; readonly status: "uploading" | "ready" | "failed"; readonly progress: number; readonly uploaded?: MessageAttachment; @@ -61,19 +64,38 @@ export function createConversationComposerActions( conversation: ConversationController, upload: ComposerActions["upload"], ): ComposerActions { + let failed: + { readonly clientId: string; readonly payload: string } | undefined; return { upload, - send: (draft, signal) => - conversation.send( - { - text: draft.text, - ...(draft.replyTo === undefined ? {} : { replyTo: draft.replyTo }), - ...(draft.attachments.length === 0 - ? {} - : { attachments: draft.attachments }), - }, - signal, - ), + send: async (draft, signal) => { + const input = { + text: draft.text, + ...(draft.replyTo === undefined ? {} : { replyTo: draft.replyTo }), + ...(draft.attachments.length === 0 + ? {} + : { attachments: draft.attachments }), + }; + const payload = JSON.stringify(input); + const retryable = + failed?.payload === payload && + conversation + .getSnapshot() + .messages.some( + (message) => + message.clientId === failed?.clientId && + message.status === "failed", + ); + const result = + retryable && failed + ? await conversation.retry(failed.clientId, signal) + : await conversation.send(input, signal); + if (result.status === "failed") { + failed = { clientId: result.clientId ?? result.id, payload }; + throw new Error(result.error ?? "Message send failed."); + } + failed = undefined; + }, }; } @@ -89,6 +111,8 @@ export interface MessageComposerSnapshot extends ControllerSnapshot { export interface MessageComposerOptions { readonly maxTextLength?: number; readonly maxAttachmentSize?: number; + /** Quality new pictures start with. Defaults to `standard`. */ + readonly defaultMediaQuality?: MediaQuality; readonly now?: () => number; } @@ -96,6 +120,7 @@ export class MessageComposerController extends ObservableController(); #sendAbort: AbortController | undefined; @@ -107,6 +132,28 @@ export class MessageComposerController extends ObservableController item.id === id); + if ( + target === undefined || + !isPicture(target) || + target.quality === quality + ) + return; + this.transition({ + ...composerFields(current), + attachments: current.attachments.map((item) => + item.id === id ? { ...item, quality } : item, + ), + }); } setText(text: string): void { @@ -127,6 +174,7 @@ export class MessageComposerController extends ObservableController - uploaded === undefined ? [] : [uploaded], + // Standard is the default send quality, so only HD is spelled out. + attachments: current.attachments.flatMap(({ uploaded, quality }) => + uploaded === undefined + ? [] + : [quality === "hd" ? { ...uploaded, quality } : uploaded], ), }, abort.signal, @@ -290,7 +341,12 @@ export class MessageComposerController extends ObservableController - item.id === attachment.id ? attachment : item, + item.id === attachment.id + ? // A quality chosen while uploading survives the upload result. + item.quality === undefined + ? attachment + : { ...attachment, quality: item.quality } + : item, ) : [...current.attachments, attachment], }); @@ -308,6 +364,11 @@ function composerFields(snapshot: MessageComposerSnapshot) { } as const; } +function isPicture(attachment: { readonly contentType: string }): boolean { + const type = attachment.contentType.toLowerCase(); + return type.startsWith("image/") && type !== "image/gif"; +} + function errorMessage(cause: unknown): string { return cause instanceof Error ? cause.message : "Composer request failed."; } diff --git a/packages/browser/src/chat/conversation.ts b/packages/browser/src/chat/conversation.ts index 80028775..bb3de489 100644 --- a/packages/browser/src/chat/conversation.ts +++ b/packages/browser/src/chat/conversation.ts @@ -5,6 +5,53 @@ import { export type MessageDirection = "inbound" | "outbound"; export type MessageStatus = "pending" | "sent" | "failed"; +export type ConversationMessageKind = + "text" | "image" | "video" | "audio" | "document" | "order"; +export type MessageContentState = + "available" | "not_retained" | "unavailable" | "deleted"; + +/** A received cart. Prices are the reported unit prices, never computed totals. */ +export interface OrderItem { + readonly productRetailerId: string; + readonly quantity: number; + readonly itemPrice: number | null; + readonly currency: string | null; +} + +export type MessageOrder = + | "unreadable" + | { + readonly catalogId: string; + readonly items: readonly OrderItem[]; + readonly text?: string; + }; + +export type MediaQuality = "standard" | "hd"; + +/** A small embedded JPEG, as carried inside WhatsApp media and link previews. */ +export interface MediaThumbnail { + readonly contentType: "image/jpeg"; + /** Base64 JPEG bytes. */ + readonly data: string; + readonly width?: number; + readonly height?: number; +} + +/** Another quality of the same picture or video, such as its HD upload. */ +export interface MediaVariant { + readonly url?: string; + readonly previewUrl?: string; + readonly size?: number; + readonly width?: number; + readonly height?: number; +} + +export interface LinkPreview { + readonly url: string; + readonly title?: string; + readonly description?: string; + readonly thumbnail?: MediaThumbnail; +} export interface MessageAttachment { readonly id: string; @@ -15,6 +62,29 @@ export interface MessageAttachment { readonly url?: string; /** A smaller image to show inline; falls back to `url` for images. */ readonly previewUrl?: string; + // The fields below are optional presentation hints from the data source. + // Renderers never infer them: no duration means none is shown, and no + // waveform means a plain track. + /** Recorded as a voice note rather than sent as an audio file. */ + readonly voice?: boolean; + /** Audio or video length in seconds. */ + readonly durationSeconds?: number; + /** Observed voice-note levels, each from 0 to 1. */ + readonly waveform?: readonly number[]; + /** Page count of a document. */ + readonly pageCount?: number; + /** Pixel size of a picture or video. */ + readonly width?: number; + readonly height?: number; + /** The embedded preview, shown blurred until the full file loads. */ + readonly thumbnail?: MediaThumbnail; + /** + * The quality this attachment was sent at. On an outgoing draft, `hd` asks + * the send adapter for an HD pair: a standard picture first, then the HD one. + */ + readonly quality?: MediaQuality; + /** The HD upload paired with this standard-quality picture or video. */ + readonly hd?: MediaVariant; } export interface ConversationMessage { @@ -24,9 +94,23 @@ export interface ConversationMessage { readonly createdAt: number; readonly direction: MessageDirection; readonly status: MessageStatus; + /** An observed type, including media whose bytes were not retained. */ + readonly messageType?: ConversationMessageKind; + /** Omission means available. Empty text alone never means missing content. */ + readonly contentState?: MessageContentState; + readonly order?: MessageOrder; readonly replyTo?: string; readonly attachments?: readonly MessageAttachment[]; + /** The preview WhatsApp attached to a link in the text. */ + readonly linkPreview?: LinkPreview; readonly error?: string; + /** Observed receipts supplied by the data source. Sending never infers them. */ + readonly receipt?: { + readonly state?: "delivered" | "read" | "played"; + readonly deliveredAt?: number; + readonly readAt?: number; + readonly playedAt?: number; + }; } export interface OutgoingMessage { @@ -90,17 +174,19 @@ export class ConversationController extends ObservableController this.#receive(event), ); } catch (cause) { - if (!this.#abort.signal.aborted) + if (!request.signal.aborted) this.transition({ status: "error", messages: [], @@ -111,22 +197,25 @@ export class ConversationController extends ObservableController { + this.assertActive(); + const request = this.#abort; const current = this.getSnapshot(); if (current.status === "loading_more" || current.cursor === undefined) return; this.transition({ ...conversationFields(current), status: "loading_more" }); try { - const page = await this.#source.load(current.cursor, this.#abort.signal); - if (this.#abort.signal.aborted) return; + const page = await this.#source.load(current.cursor, request.signal); + if (request.signal.aborted) return; this.transition({ status: "ready", - messages: mergeMessages(current.messages, page.messages), + messages: mergeMessages(this.getSnapshot().messages, page.messages), hasMore: page.nextCursor !== undefined, ...(page.nextCursor === undefined ? {} : { cursor: page.nextCursor }), }); } catch (cause) { + if (request.signal.aborted) return; this.transition({ - ...conversationFields(current), + ...conversationFields(this.getSnapshot()), status: "error", error: errorMessage(cause), }); @@ -140,18 +229,21 @@ export class ConversationController extends ObservableController, signal?: AbortSignal, - ): Promise { + ): Promise { const clientId = this.#createClientId(); - await this.#send({ ...input, clientId }, false, signal); + return this.#send({ ...input, clientId }, false, signal); } - async retry(clientId: string): Promise { + async retry( + clientId: string, + signal?: AbortSignal, + ): Promise { const message = this.getSnapshot().messages.find( (candidate) => candidate.clientId === clientId, ); if (message === undefined || message.status !== "failed") throw new Error(`Failed message ${clientId} was not found.`); - await this.#send( + return this.#send( { clientId, text: message.text, @@ -161,6 +253,7 @@ export class ConversationController extends ObservableController { + ): Promise { this.assertActive(); const current = this.getSnapshot(); const optimistic: ConversationMessage = { @@ -197,24 +290,29 @@ export class ConversationController extends ObservableController item.productRetailerId), + ].join(" "); +} + +export function conversationMessageKind( + message: ConversationMessage, +): ConversationMessageKind { + if (message.messageType) return message.messageType; + if (message.order) return "order"; + const attachment = message.attachments?.[0]; + if (!attachment) return "text"; + if (attachment.contentType.startsWith("image/")) return "image"; + if (attachment.contentType.startsWith("video/")) return "video"; + if (attachment.contentType.startsWith("audio/")) return "audio"; + return "document"; +} + +/** Search only the loaded, available content. Never requests or retains history. */ +export function messageMatchesFilters( + message: ConversationMessage, + filters: MessageFilters, + locale?: string, +): boolean { + if ( + filters.type !== "all" && + conversationMessageKind(message) !== filters.type + ) + return false; + if (filters.direction !== "all" && message.direction !== filters.direction) + return false; + if (filters.query === "") return true; + if (message.contentState && message.contentState !== "available") + return false; + return [ + message.text, + ...(message.attachments ?? []).map((file) => file.name), + orderSearchText(message.order), + ] + .join(" ") + .toLocaleLowerCase(locale) + .includes(filters.query.toLocaleLowerCase(locale)); +} + +/** Framework-neutral local filter state. Changing it never reloads or cancels a conversation. */ +export class MessageFilterController extends ObservableController { + constructor(initial: Partial = {}) { + super({ ...DEFAULT_MESSAGE_FILTERS, ...initial }); + } + setQuery(query: string): void { + this.update({ query }); + } + setType(type: MessageFilters["type"]): void { + this.update({ type }); + } + setDirection(direction: MessageFilters["direction"]): void { + this.update({ direction }); + } + clear(): void { + this.transition(DEFAULT_MESSAGE_FILTERS); + } + matches(message: ConversationMessage, locale?: string): boolean { + return messageMatchesFilters(message, this.getSnapshot(), locale); + } + private update(next: Partial): void { + const current = this.getSnapshot(); + this.transition({ + query: current.query, + type: current.type, + direction: current.direction, + ...next, + }); + } +} diff --git a/packages/browser/src/chat/session.ts b/packages/browser/src/chat/session.ts new file mode 100644 index 00000000..f09f84c7 --- /dev/null +++ b/packages/browser/src/chat/session.ts @@ -0,0 +1,67 @@ +import { + ConversationController, + type ConversationControllerOptions, + type ConversationDataSource, +} from "./conversation.js"; +import { + MessageComposerController, + createConversationComposerActions, + type ComposerActions, + type MessageComposerOptions, +} from "./composer.js"; + +export interface ConversationSessionOptions { + /** The host supplies an authorized public upload adapter, or leaves attachments disabled. */ + readonly upload?: ComposerActions["upload"]; + readonly draft?: string; + /** + * Set when this session cannot send, such as before send access is granted. + * Submitting then fails with this message, keeps the draft and adds no + * failed message to the thread. + */ + readonly sendUnavailable?: string; + readonly conversation?: ConversationControllerOptions; + readonly composer?: MessageComposerOptions; +} + +/** + * Owns the history and composer as one in-memory lifetime. Keep a session while + * switching threads to preserve its draft, attachments, reply and retry identity. + * Dispose it on scope or authority changes. This factory does not discover hosts, + * mint credentials, connect a private route, or persist anything in browser storage. + */ +export function createConversationSession( + source: ConversationDataSource, + options: ConversationSessionOptions = {}, +) { + const conversation = new ConversationController(source, options.conversation); + const actions = createConversationComposerActions( + conversation, + options.upload ?? + (async () => { + throw new Error("An upload adapter is required for attachments."); + }), + ); + const unavailable = options.sendUnavailable; + const composer = new MessageComposerController( + unavailable === undefined + ? actions + : { + ...actions, + send: async () => { + throw new Error(unavailable); + }, + }, + options.composer, + ); + if (options.draft !== undefined) composer.setText(options.draft); + return { + conversation, + composer, + dispose() { + composer.dispose(); + conversation.dispose(); + }, + }; +} +export type ConversationSession = ReturnType; diff --git a/packages/browser/src/dropin/connect.ts b/packages/browser/src/dropin/connect.ts index 2d17351d..8ee49630 100644 --- a/packages/browser/src/dropin/connect.ts +++ b/packages/browser/src/dropin/connect.ts @@ -43,7 +43,7 @@ export async function connectWhatsApp( if ( link === undefined || typeof link.url !== "string" || - !isHttpsUrl(link.url) || + !isQuickLinkUrl(link.url) || typeof link.id !== "string" ) throw new BrowserConfigurationError( @@ -67,7 +67,12 @@ export async function connectWhatsApp( } } -function isHttpsUrl(value: string): boolean { +/** + * Whether a QuickLink button may open `value`: an HTTPS URL, or HTTP on + * `localhost`/`127.0.0.1` for local development against a sandbox API. + */ +export function isQuickLinkUrl(value: unknown): value is string { + if (typeof value !== "string") return false; try { const url = new URL(value); return ( @@ -80,3 +85,22 @@ function isHttpsUrl(value: string): boolean { return false; } } + +const warnedQuickLink = new Set(); + +/** + * Warns once per component in development that a supplied QuickLink URL was + * rejected. The URL itself is never logged: it is the recipient's credential. + * Shared by the React and Web Component QuickLink buttons, which do not call + * it for a missing or blank URL. + */ +export function warnInvalidQuickLink(component: string): void { + const env = ( + globalThis as { process?: { env?: Record } } + ).process?.env?.NODE_ENV; + if (env === "production" || warnedQuickLink.has(component)) return; + warnedQuickLink.add(component); + console.warn( + `<${component}/>: hidden; url must be an https QuickLink URL your server created.`, + ); +} diff --git a/packages/browser/src/dropin/inbox.ts b/packages/browser/src/dropin/inbox.ts index d0f13ab6..e30c262c 100644 --- a/packages/browser/src/dropin/inbox.ts +++ b/packages/browser/src/dropin/inbox.ts @@ -19,10 +19,18 @@ export interface InboxConversation { readonly name?: string; readonly phoneNumber?: string; readonly avatarUrl?: string; + /** Observed organization from the host source. UI actions must await that source. */ + readonly pinned?: boolean; + readonly archived?: boolean; + readonly markedUnread?: boolean; + /** Owning Number or another short scope label. */ + readonly subtitle?: string; readonly lastMessage?: { readonly text: string; readonly createdAt: number; readonly direction: "inbound" | "outbound"; + readonly status?: ConversationMessage["status"]; + readonly receipt?: ConversationMessage["receipt"]; }; /** Epoch milliseconds of the newest activity. */ readonly lastActivity: number; @@ -422,10 +430,10 @@ export function createHandlerInboxSource( : { quotedMessage: { id: outgoing.replyTo } }), } as Parameters[0]; const request = () => - messaging.messages.send( - body, - signal === undefined ? {} : { signal }, - ); + messaging.messages.send(body, { + ...(signal === undefined ? {} : { signal }), + idempotencyKey: outgoing.clientId, + }); let response; try { response = await request(); diff --git a/packages/browser/src/dropin/index.ts b/packages/browser/src/dropin/index.ts index e4ff1709..5249ceb1 100644 --- a/packages/browser/src/dropin/index.ts +++ b/packages/browser/src/dropin/index.ts @@ -27,6 +27,8 @@ export { } from "./inbox.js"; export { connectWhatsApp, + isQuickLinkUrl, + warnInvalidQuickLink, type ConnectWhatsAppOptions, type ConnectWhatsAppResult, } from "./connect.js"; diff --git a/packages/browser/test/call-permission.test.ts b/packages/browser/test/call-permission.test.ts index 775f6a76..4adab6e9 100644 --- a/packages/browser/test/call-permission.test.ts +++ b/packages/browser/test/call-permission.test.ts @@ -63,7 +63,7 @@ describe("browser call permission messages", () => { servers.push(server); const client = new BrowserMessagingClient({ session: "support", - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", baseUrl: server.url, maxNetworkRetries: 0, }); @@ -79,7 +79,7 @@ describe("browser call permission messages", () => { method: "POST", path: "/messaging/support/messages/send", headers: { - authorization: "Bearer pmfa_ct_fixture", + authorization: "Bearer pmfa_ct_Zml4dHVyZQ", "idempotency-key": "permission-request-1", }, }); @@ -110,7 +110,7 @@ describe("browser call permission messages", () => { servers.push(server); const client = new BrowserMessagingClient({ session: "support", - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", baseUrl: server.url, maxNetworkRetries: 0, }); diff --git a/packages/browser/test/calls-client.test.ts b/packages/browser/test/calls-client.test.ts index ab10aada..1f8c959e 100644 --- a/packages/browser/test/calls-client.test.ts +++ b/packages/browser/test/calls-client.test.ts @@ -872,19 +872,19 @@ describe("browser widget and shared calls client", () => { const getClientToken = vi .fn() .mockResolvedValueOnce({ - value: "pmfa_ct_first", + value: "pmfa_ct_Zmlyc3Q", audience: "browser", expiresAt: Date.now() + 900_000, }) .mockResolvedValueOnce({ - value: "pmfa_ct_second", + value: "pmfa_ct_c2Vjb25k", audience: "browser", expiresAt: Date.now() + 900_000, }); const f = fixture({ getClientToken }); const first = await f.connect(); expect(first.url).toBe("wss://api.polymorfa.test/voip/ws"); - expect(first.texts).toEqual([{ type: "auth", token: "pmfa_ct_first" }]); + expect(first.texts).toEqual([{ type: "auth", token: "pmfa_ct_Zmlyc3Q" }]); expect(f.fetch).not.toHaveBeenCalled(); first.drop(4401, "unauthorized"); @@ -894,7 +894,7 @@ describe("browser widget and shared calls client", () => { const second = FakeWebSocket.instances[1]!; second.authenticate(); expect(second.url).toBe("wss://api.polymorfa.test/voip/ws"); - expect(second.texts).toEqual([{ type: "auth", token: "pmfa_ct_second" }]); + expect(second.texts).toEqual([{ type: "auth", token: "pmfa_ct_c2Vjb25k" }]); expect(getClientToken).toHaveBeenCalledTimes(2); expect(f.fetch).not.toHaveBeenCalled(); }); diff --git a/packages/browser/test/calls-example-numbers.test.ts b/packages/browser/test/calls-example-numbers.test.ts index c972e7e9..8c583823 100644 --- a/packages/browser/test/calls-example-numbers.test.ts +++ b/packages/browser/test/calls-example-numbers.test.ts @@ -24,7 +24,7 @@ describe("Calls example Number credential boundaries", () => { { session: "support", label: "Support", - token: "pmfa_ct_support", + token: "pmfa_ct_c3VwcG9ydA", baseUrl: "https://api.example", }, {}, @@ -34,21 +34,21 @@ describe("Calls example Number credential boundaries", () => { { session: "sales", label: "Sales", - token: "pmfa_ct_sales", + token: "pmfa_ct_c2FsZXM", baseUrl: "https://api.example", }, {}, create, ); expect(options.map(({ session }) => session)).toEqual(["support", "sales"]); - expect(await options[0]!.getClientToken()).toBe("pmfa_ct_support"); - expect(await options[1]!.getClientToken()).toBe("pmfa_ct_sales"); + expect(await options[0]!.getClientToken()).toBe("pmfa_ct_c3VwcG9ydA"); + expect(await options[1]!.getClientToken()).toBe("pmfa_ct_c2FsZXM"); expect(hasActiveCall([first, second])).toBe(false); await first.dispose(); await expect(options[0]!.getClientToken()).rejects.toThrow( "Number disconnected", ); - expect(await options[1]!.getClientToken()).toBe("pmfa_ct_sales"); + expect(await options[1]!.getClientToken()).toBe("pmfa_ct_c2FsZXM"); expect(second.calls.dispose).not.toHaveBeenCalled(); }); it("refuses server keys before constructing a Calls client", () => { diff --git a/packages/browser/test/chat-session.test.ts b/packages/browser/test/chat-session.test.ts new file mode 100644 index 00000000..e727762f --- /dev/null +++ b/packages/browser/test/chat-session.test.ts @@ -0,0 +1,100 @@ +import { expect, it, vi } from "vitest"; +import { + createConversationSession, + type ConversationMessage, + type OutgoingMessage, +} from "../src/index.js"; + +it("keeps the draft and retry identity until confirmation, then uses the server receipt", async () => { + let attempts = 0; + const send = vi.fn( + async (outgoing: OutgoingMessage): Promise => { + if (attempts++ === 0) throw new Error("Connection lost"); + return { + ...outgoing, + id: "server-id", + createdAt: 23, + direction: "outbound", + status: "sent", + }; + }, + ); + const session = createConversationSession( + { + load: async () => ({ messages: [] }), + subscribe: () => () => undefined, + send, + }, + { draft: "Hello", conversation: { createClientId: () => "request-id" } }, + ); + await session.conversation.load(); + session.composer.setReplyTo("parent"); + await session.composer.submit(); + expect(session.composer.getSnapshot().error).toBe("Connection lost"); + expect(session.composer.getSnapshot().text).toBe("Hello"); + await session.composer.submit(); + expect(send).toHaveBeenCalledTimes(2); + expect( + send.mock.calls.map(([message]) => [message.clientId, message.replyTo]), + ).toEqual([ + ["request-id", "parent"], + ["request-id", "parent"], + ]); + expect(session.conversation.getSnapshot().messages).toEqual([ + expect.objectContaining({ id: "server-id", createdAt: 23, status: "sent" }), + ]); + expect(session.composer.getSnapshot().text).toBe(""); + session.dispose(); +}); + +it("aborts an in-flight history read and upload together when the owning scope closes", async () => { + let readSignal: AbortSignal | undefined; + let uploadSignal: AbortSignal | undefined; + const session = createConversationSession( + { + load: async (_, signal) => { + readSignal = signal; + return new Promise(() => {}); + }, + subscribe: () => () => undefined, + send: vi.fn(), + }, + { + upload: async (_, __, signal) => { + uploadSignal = signal; + return new Promise(() => {}); + }, + }, + ); + void session.conversation.load(); + void session.composer.addAttachment({ + id: "a", + name: "photo.png", + contentType: "image/png", + size: 1, + file: new Blob(["a"]), + }); + session.dispose(); + session.dispose(); + expect(readSignal?.aborted).toBe(true); + expect(uploadSignal?.aborted).toBe(true); +}); + +it("refuses to send while unavailable, keeping the draft and adding no failed message", async () => { + const send = vi.fn(); + const session = createConversationSession( + { + load: async () => ({ messages: [] }), + subscribe: () => () => undefined, + send, + }, + { draft: "Keep this draft", sendUnavailable: "Sending is not available" }, + ); + await session.conversation.load(); + await session.composer.submit(); + expect(session.composer.getSnapshot().error).toBe("Sending is not available"); + expect(session.composer.getSnapshot().text).toBe("Keep this draft"); + expect(session.conversation.getSnapshot().messages).toHaveLength(0); + expect(send).not.toHaveBeenCalled(); + session.dispose(); +}); diff --git a/packages/browser/test/chat.test.ts b/packages/browser/test/chat.test.ts index c20bcdb7..40683b14 100644 --- a/packages/browser/test/chat.test.ts +++ b/packages/browser/test/chat.test.ts @@ -309,3 +309,60 @@ describe("composer and conversation wiring", () => { }); }); }); + +it("keeps a failed composer draft and retries the same operation; edits create a new operation", async () => { + const fixture = fixtureSource(); + vi.mocked(fixture.source.send).mockRejectedValueOnce(new Error("offline")); + let sequence = 0; + const conversation = new ConversationController(fixture.source, { + createClientId: () => `op-${++sequence}`, + }); + await conversation.load(); + const composer = new MessageComposerController( + createConversationComposerActions(conversation, vi.fn()), + ); + composer.setText("Keep my draft"); + composer.setReplyTo("original"); + await composer.submit(); + expect(composer.getSnapshot()).toMatchObject({ + text: "Keep my draft", + replyTo: "original", + error: "offline", + }); + await composer.submit(); + expect( + vi.mocked(fixture.source.send).mock.calls.map((call) => call[0].clientId), + ).toEqual(["op-1", "op-1"]); + expect(composer.getSnapshot().text).toBe(""); + vi.mocked(fixture.source.send).mockRejectedValueOnce(new Error("offline")); + composer.setText("another"); + await composer.submit(); + composer.setText("edited"); + await composer.submit(); + expect( + vi + .mocked(fixture.source.send) + .mock.calls.slice(-2) + .map((call) => call[0].clientId), + ).toEqual(["op-2", "op-3"]); + composer.dispose(); + conversation.dispose(); +}); + +it("ignores stale history that resolves after a newer load", async () => { + const fixture = fixtureSource(); + let complete!: (value: { messages: ReturnType[] }) => void; + vi.mocked(fixture.source.load).mockImplementationOnce( + () => + new Promise((resolve) => { + complete = resolve; + }), + ); + const conversation = new ConversationController(fixture.source); + const stale = conversation.load(); + await conversation.load(); + complete({ messages: [message("stale")] }); + await stale; + expect(conversation.getSnapshot().messages).toEqual([]); + conversation.dispose(); +}); diff --git a/packages/browser/test/client-actions.test.ts b/packages/browser/test/client-actions.test.ts index a4d48d35..50c22c8c 100644 --- a/packages/browser/test/client-actions.test.ts +++ b/packages/browser/test/client-actions.test.ts @@ -41,7 +41,7 @@ describe("BrowserMessagingClient", () => { const headers: Headers[] = []; const client = new BrowserMessagingClient({ session: "support", - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", baseUrl: "https://api.example.test", maxNetworkRetries: 0, fetch: vi.fn(async (_url, init) => { @@ -70,7 +70,7 @@ describe("BrowserMessagingClient", () => { }> = []; const client = new BrowserMessagingClient({ session: "support/eu", - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", baseUrl: "https://api.example.test", maxNetworkRetries: 0, fetch: vi.fn(async (url, init) => { @@ -125,7 +125,7 @@ describe("BrowserMessagingClient", () => { const urls: string[] = []; const client = new BrowserMessagingClient({ session: "support", - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", baseUrl: "https://api.example.test", fetch: async (url) => { urls.push(String(url)); @@ -162,7 +162,7 @@ describe("BrowserMessagingClient", () => { }> = []; const client = new BrowserMessagingClient({ session: "widget-1", - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", baseUrl: "https://api.example.test", fetch: async (url, init) => { requests.push({ url: String(url), init }); @@ -190,7 +190,7 @@ describe("BrowserMessagingClient", () => { const bodies: string[] = []; const client = new BrowserMessagingClient({ session: "support", - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch: async (url, init) => { void url; bodies.push(String(init?.body)); diff --git a/packages/browser/test/dropin.test.ts b/packages/browser/test/dropin.test.ts index 385bf1b9..1ed922e7 100644 --- a/packages/browser/test/dropin.test.ts +++ b/packages/browser/test/dropin.test.ts @@ -3,6 +3,7 @@ import { InboxController, PolymorfaClient, connectWhatsApp, + isQuickLinkUrl, createHandlerInboxSource, type EventSourceLike, type PolymorfaGrant, @@ -16,7 +17,7 @@ const grant: PolymorfaGrant = { function tokenResponse(expiresAt: number, allow = grant.allow): Response { return Response.json({ - value: "pmfa_ct_fixture", + value: "pmfa_ct_Zml4dHVyZQ", audience: "browser", expiresAt, grant: { ...grant, allow }, @@ -75,7 +76,7 @@ describe("PolymorfaClient token refresh", () => { expect(client.can("voip_place")).toBe(false); expect(client.canRead("chat_1")).toBe(true); expect(client.handlerPath).toBe("/api/polymorfa"); - expect((await client.getClientToken()).value).toBe("pmfa_ct_fixture"); + expect((await client.getClientToken()).value).toBe("pmfa_ct_Zml4dHVyZQ"); expect(fetch).toHaveBeenCalledTimes(1); }); @@ -174,7 +175,7 @@ describe("PolymorfaClient token refresh", () => { const time = clock(); const fetch = vi.fn(async () => Response.json({ - value: "pmfa_ct_x", + value: "pmfa_ct_eA", audience: "browser", expiresAt: time.now() + 60_000, }), @@ -376,6 +377,44 @@ describe("createHandlerInboxSource", () => { await source.contact!({ id: "chat_1", lastActivity: 0, unreadCount: 0 }), ).toBeUndefined(); }); + + it("reuses the same operation key across acknowledged-send retries", async () => { + const { client } = await readyClient({}); + const messaging = client.messaging("support"); + const send = vi.spyOn(messaging.messages, "send").mockResolvedValue({ + data: { data: { id: "ack-1" } }, + } as Awaited>); + vi.spyOn(client, "messaging").mockReturnValue(messaging); + const source = createHandlerInboxSource(client, { + live: false, + }).conversation({ + id: "recipient", + lastActivity: 0, + unreadCount: 0, + }); + const draft = { clientId: "operation-1", text: "Hello" }; + const signal = new AbortController().signal; + await source.send(draft, signal); + await source.send(draft, signal); + expect(send).toHaveBeenCalledTimes(2); + for (const [, options] of send.mock.calls) { + expect(options).toEqual({ idempotencyKey: "operation-1", signal }); + } + send.mockRestore(); + client.dispose(); + }); +}); + +describe("isQuickLinkUrl", () => { + it("accepts https and local development URLs only", () => { + expect(isQuickLinkUrl("https://link.polymorfa.com/ql_1")).toBe(true); + expect(isQuickLinkUrl("http://localhost:3000/connect/t")).toBe(true); + expect(isQuickLinkUrl("http://127.0.0.1/connect/t")).toBe(true); + expect(isQuickLinkUrl("http://example.com/connect/t")).toBe(false); + expect(isQuickLinkUrl("javascript:alert(1)")).toBe(false); + expect(isQuickLinkUrl("/connect/t")).toBe(false); + expect(isQuickLinkUrl(null)).toBe(false); + }); }); describe("connectWhatsApp", () => { diff --git a/packages/browser/test/message-filter.test.ts b/packages/browser/test/message-filter.test.ts new file mode 100644 index 00000000..1a48080e --- /dev/null +++ b/packages/browser/test/message-filter.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it, vi } from "vitest"; +import { + MessageFilterController, + conversationMessageKind, + messageMatchesFilters, + type ConversationMessage, +} from "../src/index.js"; + +const text: ConversationMessage = { + id: "one", + text: "Olá, Ana", + createdAt: 1, + direction: "inbound", + status: "sent", +}; + +describe("loaded message filters", () => { + it("combines query, media kind and direction without mutating message content", () => { + const filter = new MessageFilterController(); + const changed = vi.fn(); + const unsubscribe = filter.subscribe(changed); + filter.setQuery("ANA"); + expect(filter.matches(text, "pt-BR")).toBe(true); + filter.setDirection("outbound"); + expect(filter.matches(text)).toBe(false); + filter.setDirection("inbound"); + filter.setType("image"); + expect(filter.matches(text)).toBe(false); + const image = { + ...text, + text: "", + messageType: "image" as const, + attachments: [ + { id: "a", name: "Ana.png", contentType: "image/png", size: 10 }, + ], + }; + expect(filter.matches(image)).toBe(true); + filter.clear(); + expect(filter.matches(text)).toBe(true); + expect(changed).toHaveBeenCalledTimes(5); + expect(text.text).toBe("Olá, Ana"); + unsubscribe(); + filter.dispose(); + }); + it("finds orders and attachment names while excluding unavailable content from search", () => { + const cart = { + ...text, + text: "", + order: { + catalogId: "catalog", + items: [ + { + productRetailerId: "blue-shirt", + quantity: 2, + itemPrice: 1.235, + currency: "USD", + }, + ], + text: "Gift", + }, + }; + expect(conversationMessageKind(cart)).toBe("order"); + expect( + messageMatchesFilters(cart, { + query: "SHIRT", + type: "order", + direction: "all", + }), + ).toBe(true); + expect( + messageMatchesFilters( + { ...cart, contentState: "not_retained" }, + { query: "shirt", type: "all", direction: "all" }, + ), + ).toBe(false); + expect( + messageMatchesFilters( + { ...text, text: "" }, + { query: "", type: "text", direction: "all" }, + ), + ).toBe(true); + }); +}); diff --git a/packages/browser/test/package.test.ts b/packages/browser/test/package.test.ts index 5f9968b6..e96c0449 100644 --- a/packages/browser/test/package.test.ts +++ b/packages/browser/test/package.test.ts @@ -70,13 +70,13 @@ describe("@polymorfa/browser package", () => { 'import { BrowserMessagingClient, BrowserTransport, ClientTokenManager, createSameOriginTemplateBuilderTransport } from "@polymorfa/browser";', 'import { CallsSignalingClient } from "@polymorfa/browser/internal";', 'import { CallsDisabledError } from "@polymorfa/sdk/calls";', - 'const token = await new ClientTokenManager(async () => "pmfa_ct_fixture").get();', + 'const token = await new ClientTokenManager(async () => "pmfa_ct_Zml4dHVyZQ").get();', 'const client = new BrowserMessagingClient({ session: "support", getClientToken: async () => token });', "const templates = createSameOriginTemplateBuilderTransport();", // A browser request refused because calling is off raises the one // CallsDisabledError class applications import from the SDK. "const transport = new BrowserTransport({", - ' getClientToken: async () => "pmfa_ct_fixture",', + ' getClientToken: async () => "pmfa_ct_Zml4dHVyZQ",', ' baseUrl: "https://api.polymorfa.test",', " maxNetworkRetries: 0,", ' fetch: async () => new Response(JSON.stringify({ error: { code: "calls_disabled", message: "Calling is off" } }), { status: 403, headers: { "content-type": "application/json" } }),', @@ -92,7 +92,7 @@ describe("@polymorfa/browser package", () => { }); expect(imported.status, imported.stderr).toBe(0); expect(imported.stdout.trim()).toBe( - "pmfa_ct_fixture function function true", + "pmfa_ct_Zml4dHVyZQ function function true", ); const manifest = JSON.parse( readFileSync( diff --git a/packages/browser/test/token.test.ts b/packages/browser/test/token.test.ts index 7a9a0402..f1ff1d83 100644 --- a/packages/browser/test/token.test.ts +++ b/packages/browser/test/token.test.ts @@ -33,15 +33,15 @@ describe("ClientTokenManager", () => { const second = manager.get(); expect(provider).toHaveBeenCalledTimes(1); resolveToken?.({ - value: "pmfa_ct_fixture", + value: "pmfa_ct_Zml4dHVyZQ", audience: "browser", expiresAt: 5_000, }); await expect(Promise.all([first, second])).resolves.toEqual([ - "pmfa_ct_fixture", - "pmfa_ct_fixture", + "pmfa_ct_Zml4dHVyZQ", + "pmfa_ct_Zml4dHVyZQ", ]); - await expect(manager.get()).resolves.toBe("pmfa_ct_fixture"); + await expect(manager.get()).resolves.toBe("pmfa_ct_Zml4dHVyZQ"); expect(provider).toHaveBeenCalledTimes(1); }); @@ -60,29 +60,29 @@ describe("ClientTokenManager", () => { const joined = manager.get(); expect(provider).toHaveBeenCalledTimes(2); pending[1]!({ - value: "pmfa_ct_fresh", + value: "pmfa_ct_ZnJlc2g", audience: "browser", expiresAt: 90_000, }); await expect(refreshed).resolves.toEqual({ - value: "pmfa_ct_fresh", + value: "pmfa_ct_ZnJlc2g", expiresAt: 90_000, }); - await expect(joined).resolves.toBe("pmfa_ct_fresh"); + await expect(joined).resolves.toBe("pmfa_ct_ZnJlc2g"); // The older call settling later does not replace the refreshed token. pending[0]!({ - value: "pmfa_ct_revoked", + value: "pmfa_ct_cmV2b2tlZA", audience: "browser", expiresAt: 95_000, }); - await expect(plain).resolves.toBe("pmfa_ct_revoked"); - await expect(manager.get()).resolves.toBe("pmfa_ct_fresh"); + await expect(plain).resolves.toBe("pmfa_ct_cmV2b2tlZA"); + await expect(manager.get()).resolves.toBe("pmfa_ct_ZnJlc2g"); expect(provider).toHaveBeenCalledTimes(2); }); it("lets a plain request share an in-flight forced refresh", async () => { const provider = vi.fn(async () => ({ - value: "pmfa_ct_fresh", + value: "pmfa_ct_ZnJlc2g", audience: "browser" as const, expiresAt: 90_000, })); @@ -91,8 +91,8 @@ describe("ClientTokenManager", () => { manager.token({ refresh: true }), manager.get(), ]); - expect(refreshed.value).toBe("pmfa_ct_fresh"); - expect(plain).toBe("pmfa_ct_fresh"); + expect(refreshed.value).toBe("pmfa_ct_ZnJlc2g"); + expect(plain).toBe("pmfa_ct_ZnJlc2g"); expect(provider).toHaveBeenCalledTimes(1); }); @@ -101,12 +101,12 @@ describe("ClientTokenManager", () => { const provider = vi .fn() .mockResolvedValueOnce({ - value: "pmfa_ct_first", + value: "pmfa_ct_Zmlyc3Q", audience: "browser", expiresAt: 1_200, }) .mockResolvedValueOnce({ - value: "pmfa_ct_second", + value: "pmfa_ct_c2Vjb25k", audience: "browser", expiresAt: 5_000, }); @@ -114,21 +114,21 @@ describe("ClientTokenManager", () => { now: () => now, expirySkewMs: 100, }); - await expect(manager.get()).resolves.toBe("pmfa_ct_first"); + await expect(manager.get()).resolves.toBe("pmfa_ct_Zmlyc3Q"); now = 1_101; - await expect(manager.get()).resolves.toBe("pmfa_ct_second"); + await expect(manager.get()).resolves.toBe("pmfa_ct_c2Vjb25k"); }); it("rejects wrong prefixes, expired tokens, and non-browser audiences", async () => { for (const token of [ { value: "pmfa_server", audience: "browser" as const, expiresAt: 5_000 }, { - value: "pmfa_ct_expired", + value: "pmfa_ct_ZXhwaXJlZA", audience: "browser" as const, expiresAt: 999, }, { - value: "pmfa_ct_server", + value: "pmfa_ct_c2VydmVy", audience: "server" as const, expiresAt: 5_000, }, @@ -147,7 +147,7 @@ describe("createClientTokenProvider", () => { it("posts to a relative application route and returns browser claims", async () => { const fetch = vi.fn(async () => Response.json({ - value: "pmfa_ct_route", + value: "pmfa_ct_cm91dGU", audience: "browser", expiresAt: 5_000, }), @@ -158,7 +158,7 @@ describe("createClientTokenProvider", () => { }); await expect(provider()).resolves.toEqual({ - value: "pmfa_ct_route", + value: "pmfa_ct_cm91dGU", audience: "browser", expiresAt: 5_000, }); diff --git a/packages/browser/test/transport.test.ts b/packages/browser/test/transport.test.ts index ed58b024..7b96df72 100644 --- a/packages/browser/test/transport.test.ts +++ b/packages/browser/test/transport.test.ts @@ -16,7 +16,7 @@ describe("BrowserTransport", () => { const diagnostics: BrowserDiagnosticEvent[] = []; let receivedAuthorization = ""; const transport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch: async (_url, init) => { receivedAuthorization = new Headers(init?.headers).get("authorization") ?? ""; @@ -37,13 +37,13 @@ describe("BrowserTransport", () => { headers: { authorization: "Bearer attacker", "x-trace": "fixture" }, }); - expect(receivedAuthorization).toBe("Bearer pmfa_ct_fixture"); + expect(receivedAuthorization).toBe("Bearer pmfa_ct_Zml4dHVyZQ"); expect(response.metadata).toMatchObject({ status: 200, requestId: "req_browser", attempts: 1, }); - expect(JSON.stringify(diagnostics)).not.toContain("pmfa_ct_fixture"); + expect(JSON.stringify(diagnostics)).not.toContain("pmfa_ct_Zml4dHVyZQ"); expect(diagnostics.map(({ type }) => type)).toEqual([ "request.started", "request.completed", @@ -64,7 +64,7 @@ describe("BrowserTransport", () => { ); const client = new BrowserMessagingClient({ session: "number/1", - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch, }); const response = await client.messages.send({ @@ -76,7 +76,7 @@ describe("BrowserTransport", () => { ); const headers = new Headers(fetch.mock.calls[0]?.[1]?.headers); expect(headers.get("polymorfa-version")).toBe("2026-09-22"); - expect(headers.get("authorization")).toBe("Bearer pmfa_ct_fixture"); + expect(headers.get("authorization")).toBe("Bearer pmfa_ct_Zml4dHVyZQ"); expect(response.data.data).toMatchObject({ whatsapp_ids: { linked_devices: "provider-1" }, whatsapp_id: "provider-1", @@ -99,7 +99,7 @@ describe("BrowserTransport", () => { ); const client = new BrowserMessagingClient({ session: "number", - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch, maxNetworkRetries: 2, }); @@ -121,7 +121,7 @@ describe("BrowserTransport", () => { it("excludes raw Graph and application routes from the native default", async () => { const fetch = vi.fn(async () => Response.json({})); const transport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch, }); await transport.request({ @@ -163,7 +163,7 @@ describe("BrowserTransport", () => { .mockResolvedValueOnce(new Response('{"error":"busy"}', { status: 503 })) .mockResolvedValueOnce(new Response('{"ok":true}', { status: 200 })); const getTransport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch: getFetch, maxNetworkRetries: 1, sleep: async () => undefined, @@ -178,7 +178,7 @@ describe("BrowserTransport", () => { .fn() .mockResolvedValue(new Response('{"error":"busy"}', { status: 503 })); const postTransport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch: postFetch, maxNetworkRetries: 2, sleep: async () => undefined, @@ -204,7 +204,7 @@ describe("BrowserTransport", () => { ), ); const transport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch: fetcher, maxNetworkRetries: 2, sleep: async () => undefined, @@ -231,7 +231,7 @@ describe("BrowserTransport", () => { ), ); const transport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch: fetcher, maxNetworkRetries: 2, sleep: async () => undefined, @@ -272,7 +272,7 @@ describe("BrowserTransport", () => { ), ); const transport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch: fetcher, maxNetworkRetries: 2, sleep: async () => undefined, @@ -324,7 +324,7 @@ describe("BrowserTransport", () => { ), ); const transport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch: fetcher, maxNetworkRetries: 2, sleep: async () => undefined, @@ -354,7 +354,7 @@ describe("BrowserTransport", () => { ), ); const transport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch: pendingFetch, }); const controller = new AbortController(); @@ -378,7 +378,7 @@ describe("BrowserTransport", () => { it("rejects absolute and backslash network paths before fetching", async () => { const fetch = vi.fn(); const transport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch, }); for (const path of [ @@ -394,7 +394,7 @@ describe("BrowserTransport", () => { }); it("does not retry invalid local request options", async () => { - const getClientToken = vi.fn(async () => "pmfa_ct_fixture"); + const getClientToken = vi.fn(async () => "pmfa_ct_Zml4dHVyZQ"); const fetch = vi.fn(); const transport = new BrowserTransport({ getClientToken, @@ -410,7 +410,7 @@ describe("BrowserTransport", () => { it("reads the code, request ID, and doc link from an error body without the header", async () => { const transport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", fetch: async () => new Response( JSON.stringify({ @@ -464,7 +464,7 @@ describe("BrowserTransport fetch receiver", () => { ])("sends mutations through %s", async (_name, options) => { vi.stubGlobal("fetch", receiverCheckingFetch()); const transport = new BrowserTransport({ - getClientToken: async () => "pmfa_ct_fixture", + getClientToken: async () => "pmfa_ct_Zml4dHVyZQ", maxNetworkRetries: 0, ...options(), }); diff --git a/packages/calls/README.md b/packages/calls/README.md index 15ff867f..f104ecb6 100644 --- a/packages/calls/README.md +++ b/packages/calls/README.md @@ -38,7 +38,7 @@ const call = await client.place("+15550100", { video: false }); - Browser and other untrusted clients use a client token. Your server mints it with `POST /platform/client-tokens` and grants `voip_place`, `voip_answer`, and `voip_signal`. The token decides the session and the participant. -- Server applications use an organization API key or project token. They may +- Server applications use an team key or project token. They may set `participant` (`[A-Za-z0-9._:@-]{1,128}`, default `default`); the platform refers to them as `server:`. diff --git a/packages/calls/src/api.ts b/packages/calls/src/api.ts index d39b1ca8..f3d4e420 100644 --- a/packages/calls/src/api.ts +++ b/packages/calls/src/api.ts @@ -30,7 +30,7 @@ export interface AcceptCallOptions { */ readonly exclusive?: boolean; readonly video?: boolean; - /** Participant name for organization API keys and project tokens. */ + /** Participant name for team keys and project tokens. */ readonly participant?: string; } @@ -52,7 +52,7 @@ export interface PlaceCallRequest { readonly video: boolean; /** Claim the call for the placing participant. Default `false`. */ readonly exclusive?: boolean; - /** Participant name for organization API keys and project tokens. */ + /** Participant name for team keys and project tokens. */ readonly participant?: string; readonly idempotencyKey: string; } diff --git a/packages/calls/src/token.ts b/packages/calls/src/token.ts index 05007716..7ba6a650 100644 --- a/packages/calls/src/token.ts +++ b/packages/calls/src/token.ts @@ -16,7 +16,7 @@ export interface CallsTokenRequest { * Supplies the credential for REST calls and socket authentication. Browser * applications return a client token minted by their own server with * `POST /platform/client-tokens`. Server applications may return an - * organization API key or project token. + * team key or project token. */ export type CallsTokenProvider = ( request: CallsTokenRequest, diff --git a/packages/calls/test/diagnostics.test.ts b/packages/calls/test/diagnostics.test.ts index 40749843..afccae3d 100644 --- a/packages/calls/test/diagnostics.test.ts +++ b/packages/calls/test/diagnostics.test.ts @@ -131,7 +131,7 @@ describe("HttpCallsApi.report", () => { error: { code: "media_timeout" }, }; for (const [token, expected] of [ - ["pmfa_ct_browser", { ...report, participant: undefined }], + ["pmfa_ct_YnJvd3Nlcg", { ...report, participant: undefined }], ["pmfa_sk_server", report], ] as const) { fetch.mockClear(); diff --git a/packages/calls/test/lifecycle.test.ts b/packages/calls/test/lifecycle.test.ts index 17a48ef6..17c2eb04 100644 --- a/packages/calls/test/lifecycle.test.ts +++ b/packages/calls/test/lifecycle.test.ts @@ -61,7 +61,7 @@ describe("LifecycleSocket authentication", () => { let now = 0; const h = lifecycleWith(vi.fn(), () => now); h.api.token - .mockResolvedValueOnce({ value: "pmfa_ct_one", expiresAt: 300_000 }) + .mockResolvedValueOnce({ value: "pmfa_ct_b25l", expiresAt: 300_000 }) .mockResolvedValueOnce({ value: "pmfa_ct_two", expiresAt: 600_000 }); const connecting = h.socket.connect(); await flush(); diff --git a/packages/calls/test/social.test.ts b/packages/calls/test/social.test.ts index 0d4b25c6..296d65f5 100644 --- a/packages/calls/test/social.test.ts +++ b/packages/calls/test/social.test.ts @@ -68,7 +68,7 @@ describe("call interactions", () => { headers: { "content-type": "application/json" }, }), ); - const api = new HttpCallsApi({ token: "pmfa_ct_token", fetch }); + const api = new HttpCallsApi({ token: "pmfa_ct_dG9rZW4", fetch }); await api.sendReaction("call", "connection-1", "", "forged"); expect(JSON.parse(String(fetch.mock.calls[0]?.[1]?.body))).toEqual({ connectionId: "connection-1", diff --git a/packages/calls/test/token.test.ts b/packages/calls/test/token.test.ts index d1285b13..02558ddf 100644 --- a/packages/calls/test/token.test.ts +++ b/packages/calls/test/token.test.ts @@ -15,11 +15,11 @@ describe("CallsTokenSource", () => { const calls: boolean[] = []; const source = new CallsTokenSource(async ({ refresh }) => { calls.push(refresh === true); - return "pmfa_ct_one"; + return "pmfa_ct_b25l"; }); const [a, b] = await Promise.all([source.get(), source.get()]); - expect(a.value).toBe("pmfa_ct_one"); - expect(b.value).toBe("pmfa_ct_one"); + expect(a.value).toBe("pmfa_ct_b25l"); + expect(b.value).toBe("pmfa_ct_b25l"); expect(calls).toEqual([false]); }); @@ -28,17 +28,17 @@ describe("CallsTokenSource", () => { const calls: boolean[] = []; const source = new CallsTokenSource(async ({ refresh }) => { calls.push(refresh === true); - return refresh ? "pmfa_ct_fresh" : stale.promise; + return refresh ? "pmfa_ct_ZnJlc2g" : stale.promise; }); const plain = source.get(); const refreshed = await source.get({ refresh: true }); - expect(refreshed.value).toBe("pmfa_ct_fresh"); + expect(refreshed.value).toBe("pmfa_ct_ZnJlc2g"); expect(calls).toEqual([false, true]); // The older call finishing later does not replace the refreshed token. - stale.resolve("pmfa_ct_refused"); - expect((await plain).value).toBe("pmfa_ct_refused"); - expect((await source.get()).value).toBe("pmfa_ct_fresh"); + stale.resolve("pmfa_ct_cmVmdXNlZA"); + expect((await plain).value).toBe("pmfa_ct_cmVmdXNlZA"); + expect((await source.get()).value).toBe("pmfa_ct_ZnJlc2g"); }); it("does not let a call pending across invalidate() answer or repopulate later requests", async () => { @@ -46,16 +46,16 @@ describe("CallsTokenSource", () => { let calls = 0; const source = new CallsTokenSource(async () => { calls += 1; - return calls === 1 ? stale.promise : "pmfa_ct_fresh"; + return calls === 1 ? stale.promise : "pmfa_ct_ZnJlc2g"; }); const before = source.get(); source.invalidate(); // e.g. a 401 on a REST call const after = source.get(); expect(calls).toBe(2); - stale.resolve("pmfa_ct_refused"); - expect((await before).value).toBe("pmfa_ct_refused"); - expect((await after).value).toBe("pmfa_ct_fresh"); - expect((await source.get()).value).toBe("pmfa_ct_fresh"); + stale.resolve("pmfa_ct_cmVmdXNlZA"); + expect((await before).value).toBe("pmfa_ct_cmVmdXNlZA"); + expect((await after).value).toBe("pmfa_ct_ZnJlc2g"); + expect((await source.get()).value).toBe("pmfa_ct_ZnJlc2g"); expect(calls).toBe(2); }); @@ -79,9 +79,9 @@ describe("CallsTokenSource", () => { await expect(cancelled).rejects.toThrow("placement cancelled"); // The shared provider call is still running for the other callers. expect(signals[0]?.aborted).toBe(false); - pending.resolve("pmfa_ct_shared"); - expect((await kept).value).toBe("pmfa_ct_shared"); - expect((await unsignalled).value).toBe("pmfa_ct_shared"); + pending.resolve("pmfa_ct_c2hhcmVk"); + expect((await kept).value).toBe("pmfa_ct_c2hhcmVk"); + expect((await unsignalled).value).toBe("pmfa_ct_c2hhcmVk"); }); it("cancels the shared provider call once every caller has cancelled", async () => { @@ -112,14 +112,14 @@ describe("CallsTokenSource", () => { const calls: boolean[] = []; const source = new CallsTokenSource(async ({ refresh }) => { calls.push(refresh === true); - return "pmfa_ct_fresh"; + return "pmfa_ct_ZnJlc2g"; }); const [refreshed, plain] = await Promise.all([ source.get({ refresh: true }), source.get(), ]); - expect(refreshed.value).toBe("pmfa_ct_fresh"); - expect(plain.value).toBe("pmfa_ct_fresh"); + expect(refreshed.value).toBe("pmfa_ct_ZnJlc2g"); + expect(plain.value).toBe("pmfa_ct_ZnJlc2g"); expect(calls).toEqual([true]); }); }); diff --git a/packages/dotnet/README.md b/packages/dotnet/README.md new file mode 100644 index 00000000..eaa9b490 --- /dev/null +++ b/packages/dotnet/README.md @@ -0,0 +1,1014 @@ +# Polymorfa .NET SDK + +Handwritten server SDK for .NET 8 and later. This package is in development and has not been published to NuGet. It uses the API contract revision `2026-09-22` pinned in this repository. + +Create a messaging client with an team key or a project token. Keep server credentials in your server process. + +```csharp +using Polymorfa.Sdk; + +using var client = new MessagingClient( + Credential.OrganizationApiKey(Environment.GetEnvironmentVariable("POLYMORFA_API_KEY")!)); +var response = await client.Messages.SendAsync( + "support", + new SendMessageRequest( + new ConversationReference(PhoneNumber: "+15551234567"), + new TextContent("Hello"))); +Console.WriteLine(response.Data.Data.Id); +``` + +Organization and project clients provide immutable project views. A project token requires an explicit project ID and cannot be rebound to another project. Messaging client tokens cannot call server administration methods. + +Every response includes status, API version, request ID, attempt count, and safe response headers. Request options support cancellation, timeout, API version, custom headers, and an explicit idempotency key. Safe reads and keyed writes retry transient failures; ordinary writes do not. Message sends use a stable key throughout retries. Call links and transient call reactions never retry automatically. + +Writes that generate an idempotency key prefer `RequestOptions.IdempotencyKey`, +then a caller's case-insensitive `Idempotency-Key` header. Matching header aliases +collapse to one value; conflicting aliases are rejected unless the explicit +option supplies the key. Reusing the same caller key preserves the identity +across separate method calls and their retries. Without a caller key, each method +call receives a new key. Keys contain 1–255 UTF-8 bytes and valid header characters. +The SDK copies request headers without changing the caller's dictionary. A raw +header alone does not enable automatic retries for an unsafe request; endpoint +retry limits and replay behavior still apply. + +`Webhooks.ConstructEvent` verifies native HMAC signatures against the exact raw request bytes before parsing. `ConstructLocalEvent` separately verifies timestamped CLI forwarding signatures. Unknown event names retain their envelope and payload. Deduplicate processed event IDs in your application. `Webhooks.CreateFixture` signs exact JSON bytes for application tests. + +Typed resources include sessions, all 20 message content kinds, QuickLinks, webhooks, Business, Channels, contacts, groups, hosted history, profile, presence, labels, privacy, identities, client token rules, organization metadata, billing, customer management, project settings, BanSafe, Flows, Functions, Voice, usage, SIP, and call controls. Native HTTP tests check the methods recorded in `coverage.json`. + +Known webhooks have typed payloads for all 84 names in the pinned TypeScript contract. Unknown events retain their envelope and payload. Event streams provide asynchronous enumeration and resume from a saved cursor. `MessagingMedia.SignedUrlExpiry` reads a previously received URL's expiry without a request. Media helpers verify SHA-256 and HMAC before releasing decrypted content; API downloads use a separate credential-free storage client. Voice upload sends bytes over that independent storage transport, then completes the asset through the API. + +`Events.LiveSource` and `EventStreamSource` feed decoded webhook bodies to an application-owned store. Each subscription resumes from its saved cursor and skips events whose body was not retained. Dispose the subscription to stop delivery; await `DisposeAsync` or `Completion` to wait for stream cleanup. `OnError` receives terminal stream or listener failures. + +The existing coverage ledger records 442 covered operations. The separate campaign/custody supplement adds 30 source implementations with TCP fixtures; its ledger adoption is pending. Graph and testing-phone supplements retain their separate fixtures. This is source-level contract coverage, not a registry release or hosted acceptance result. + +`CallsClient` tracks incoming and outgoing calls. `Call` provides answer, join, reject, leave, end, participant controls and typed media frames. Connections authenticate in the first WebSocket frame, refresh expiring credentials, detect missed heartbeats and reconnect within a bounded attempt budget. Media reconnect preserves connection identity and acknowledged preferences. `CallsTokenSource` shares concurrent provider requests while preserving each caller's cancellation. PCM uses the rate announced in `Call.SampleRate`; H.264 frames retain timestamps. Outgoing H.264 uses the protocol's reserved source zero; received source ownership is exposed separately. + +Use `Call.MediaFramesAsync` for PCM, H.264, source ownership and keyframe requests, and `SendAudioAsync`/`SendVideoAsync` to send encoded media. `VideoSources` is a snapshot of the current connection's owners and clears when media disconnects or the call ends. This native media interface does not require browser tracks. + +`WhatsAppMedia.DecryptToFileAsync` and `DownloadToFileAsync` authenticate encrypted media before decrypting it into a private scratch file and atomically committing the verified result. They bound input size, clean up failures and preserve existing files unless overwrite is requested. `MessagingMedia.DownloadToFileAsync` performs bounded API media downloads over the separate credential-free storage transport. + +The [examples](examples/README.md) include an ASP.NET Core authenticated token/webhook server, Blazor WebAssembly and MAUI Blazor Hybrid hosts, and a programmatic Calls agent. Native builds verify the examples; ASP.NET and Blazor WebAssembly run in a browser against local API fixtures. The MAUI Mac Catalyst app completes sign-in, component mounting, token minting, inbox loading and logout against the same fixtures. Other MAUI platforms remain unexercised. + +Signal cryptography and key custody remain in the customer-operated Signal service; the SDK connects to its local application API. + +### Campaign, audience profiles and custody defaults + +The 30-operation campaign/custody supplement follows API source +`f97ad78b172e80788c33bdc0fcf28f75db9283ec`. These helpers use +`Polymorfa-Version: 2026-10-14` when `RequestOptions.ApiVersion` is absent. +An explicit override is preserved. Existing helpers retain their `2026-09-22` +default. This is unpublished source; the supplement remains held for release. + +`OrganizationClient.AudienceProfiles` provides `ListAsync`, `RetrieveAsync`, +`PutAsync`, `DeleteAsync` and `ListConsentAsync`. Its `Imports` resource provides +`CreateAsync`, `RetrieveAsync` and `CancelAsync`. Profiles contain customer-supplied +campaign data. They do not import WhatsApp contacts, and consent assertions do +not authorize a send. Attribute values use `AudienceProfileAttributeValue`, +including its explicit `Null`; campaign test variables use +`CampaignVariableValue` and admit string, finite number or boolean values. + +`OrganizationClient.Campaigns` provides `GetReplyFlowAsync`, +`CreateReplyFlowAsync`, `SetReplyFlowAsync`, `TestSendAsync`, +`ListLaunchApprovalsAsync`, `LinksAsync`, `ExportRecipientsAsync` and +`GetSettingsAsync`. `Audiences.CreateFromCampaignAsync` creates an outcome-based +audience. `OptOuts` provides `GetConfirmationSettingsAsync`, +`UpdateConfirmationSettingsAsync` and `GetConfirmationSummaryAsync`. + +A retained `platform.Project(projectId).Campaigns` view exposes the same seven +campaign methods with its captured project. Settings and organization profile, +import, retarget and STOP resources remain on the organization client. The six +Messaging campaign methods use a project slug and keep `SuccessEnvelope`; +all require a server principal. Platform campaign responses retain +`DataEnvelope`. Reply flows use the closed `CampaignReplyText`, +`CampaignReplyButtons`, `CampaignReplyList`, `CampaignReplyForm` send variants +and `CampaignReplyKeyword`, `CampaignReplyChoice`, `CampaignReplySubmission` +matches. Unpaired UTF-16 surrogates are refused before HTTP because the .NET serializer +would replace them. Well-formed Unicode retains the contract UTF-8 and UTF-16 +bounds. Responses preserve required nulls and reject malformed successful +responses, unknown variants and missing fields. + +```csharp +using var platform = new OrganizationClient(Credential.OrganizationApiKey(apiKey)); +using var project = platform.Project(projectId); +await project.Campaigns.SetReplyFlowAsync(campaignId, new(null)); +await project.SignalCustody.UpdateCustomerAsync(customerId, new(0, null)); +var defaults = await project.SignalCustody.RetrieveAsync(); +Console.WriteLine(defaults.Data.EffectiveSignalMode); +``` + +A null reply-flow ID detaches the flow. Customer custody null restores Project +inheritance. Project updates require a concrete `SignalCustodyMode`; these +settings affect future defaults and do not move existing Number keys. The four +bound `SignalCustody` methods unwrap the data envelope once and retain metadata. +The equivalent organization `Projects` and `Customers` aliases retain +`DataEnvelope`. Disposing a borrowed Project view does not +close its owner's transport. Project tokens cannot rebind to another project; +team keys retain their existing rebind behavior. + +Profile and consent lists return one `AudienceProfilePage` with +`Page.NextCursor` and `Page.HasMore`. Exports return one +`CampaignRecipientExportPage` with the exact CSV, next cursor and display +filename. The SDK does not fetch subsequent pages, create files, poll imports +or follow export redirects. STOP updates retain disabled `false` and required +null locale. Import replay keys remain body identity, and ordinary new writes +have no automatic replay by default even when given an HTTP key. Explicit retry +limits remain caller choices. Test sends capture one caller or generated key, +request headers and body for their retries. Cancellation stops the HTTP wait; +it does not cancel an operation already admitted by the API. + +Run the native checks from the repository root: + +```sh +node scripts/verify-native-sdk.mjs dotnet +``` + +`WhatsAppMediaDecryptor` provides native incremental CBC, MAC and hash verification. `WhatsAppMedia.DecryptStreamAsync` and `DownloadStreamAsync` return asynchronous read-only plaintext streams. The default `MediaVerifyMode.BeforeRelease` emits nothing until complete verification; `Streaming` releases unverified bytes, which must all be discarded if a later read fails. Downloads accept decoded descriptors or existing `LinkedDeviceMessagePayload` values; `MessagingMedia.DownloadFromWhatsAppStreamAsync` exposes the same helper. Dispose the stream/download even when abandoning a body. Reads enforce encrypted/plaintext limits and cancellation. `WhatsAppMediaStreamDownload.WriteToFileAsync` promotes a private partial file only after complete stream verification, preserving the destination on failure. `ConfigureHttpHandler` supports native DNS, proxy and trust-root configuration; the SDK owns cookie, credential and redirect policies, enforces HTTPS WhatsApp hosts and preserves signed URL queries. + +Receipt-timed campaign follow-ups are available in the typed Messaging and Platform +campaign requests, campaign responses, recipient summaries, and analytics reports. +A follow-up sends text or a template. Omit its timing fields to use API defaults; +returned timing fields also remain optional. Clear a draft follow-up with an explicit +null (`PatchValue.Set(null)`). +The API requires beta enrollment and a single first message, and owns receipt timing, +reply and opt-out cancellation, expiry, and send eligibility. The SDK never grants access. + +Use `PaginateRawAsync` for a raw endpoint with its own page shape. Supply a +`RawRequest`, a decoder returning `DecodedRawPage`, and an optional cursor +parameter name. `RawCursorPage` retains the response and metadata, supports +`NextPageAsync` and `await foreach`, and preserves request bodies, filters, +headers, and cancellation across pages. A project client accepts only paths +relative to its bound project. + +Voice synthesis uses provider-specific inputs: + +```csharp +using var platform = new OrganizationClient(Credential.OrganizationApiKey( + Environment.GetEnvironmentVariable("POLYMORFA_API_KEY")!)); +var speech = new OpenAiVoiceAudioInput( + "Greeting", "Hello", OpenAiTtsVoice.Alloy, OpenAiTtsModel.Gpt4oMiniTts); +var synthesis = await platform.Voice.Audio.SynthesizeAsync(speech, "project-id"); +``` + +Use `ElevenLabsVoiceAudioInput` with a provider voice ID and an +`ElevenLabsTtsModel` for ElevenLabs. These inputs replace the former string-based +`SynthesizeVoiceAudioInput` constructor. Template definitions use +`TemplateDefinitionVersion.V1`, `TemplateKind`, `TemplateCategory`, +`TemplateVariableType`, `TemplateOtpType`, and `TemplateSurface`. Carousel headers +must be `TemplateImageHeader`, `TemplateVideoHeader`, or `TemplateDocumentHeader`. +Replace numeric/string arguments with those types when updating source callers. + +The `Voice.Audio.UploadAsync` stream overload uploads as the source produces +bytes, using a bounded buffer. Declare the exact byte length and content type in +`CreateVoiceAudioUploadInput`; the maximum is 16 MiB. The SDK leaves the caller's +stream open, refuses shorter or longer input, and never completes or retries a +failed storage write. Storage requests use a separate HTTP client, contain no +API credential, and refuse redirects. + +Configured API base URLs can include a reverse-proxy path. Requests and relative download capabilities retain that prefix; a trailing slash does not change the request path. + +### Contact frequency caps, audience segments and drips (beta) + +These methods follow `contracts/campaign-frequency-cap.json` and +`contracts/campaign-segments-drip.json`. They do not enroll a team in a beta; +the API returns `403 feature_unavailable` when a team lacks access. + +`Campaigns.GetFrequencyCapAsync` returns the team limits, `Revision` and +`UpdatedAt` (null until first saved). `UpdateFrequencyCapAsync` replaces both +windows with the revision you read; null limits remove them. The PUT is sent +once, and a stale revision throws `PolymorfaConflictException`, so read again +and reconcile. Campaign create and update take `FrequencyCap` +(`PatchValue.Set(null)` removes a campaign cap), +recipients report `CampaignRecipientFailureReason.FrequencyCap`, and analytics +add `FrequencyCap.SkippedRecipientCount`. + +`OrganizationClient.AudienceSegments` lists, retrieves, creates, updates and +deletes saved segments, and previews a saved segment or unsaved rules. +`ProjectClient` has no segment resource. Conditions are records such as +`AudienceSegmentTagContains` and `AudienceSegmentAttributeCompare`; +`AudienceSegmentValue.Of` keeps each value's exact JSON spelling. Conditions the +SDK does not model decode as `AudienceSegmentUnknownCondition` and are rejected +in requests. Updates and deletes need the last observed revision. Writes and +both previews are sent once; a 409 `state_conflict` means a stale revision or an +evaluation timeout, so re-read or simplify the rules. + +Platform create takes `SegmentId` (omission only) and update takes +`SegmentId = PatchValue.Set(null)` to clear it. `Drip` takes +`CampaignDripInput` on create and a partial `CampaignDripPatch` on update; the +SDK applies no defaults and omitted fields keep their stored values. Drip steps +take `DelayUnit` and a per-step `SendWindow`. Ordinary creates allow +`DelayAfterSec` up to 86400; drip creates and every PATCH allow up to 2592000, +and the API checks the stored drip, delay unit and eligibility. `EnrollAsync` +on both campaign resources adds a contact to a continuous drip and is never +retried automatically. Campaign reads expose `Drip`, `EnrollmentOpen`, +`PauseReason`, `SegmentId` and `SegmentSnapshot`; analytics expose per-step +`Drip` results. The new fields are init-only properties, so existing positional +constructors are unchanged. + +## Customer-owned media, history and Signal service + +These unpublished helpers follow TypeScript `254fc6a2d073e7fc79420cf95f9b03927d00c04a` +and Signal service `8aa191ef0d4c318229c91bcdf474bf2d79dac47c`. Local implementation +does not establish deployed access or enrollment. New media/history operations +and descriptor sends select API revision `2026-10-14` per request; existing +operations keep their previous default. + +`WhatsAppMedia.EncryptAsync(source, destination, options)` encrypts incrementally +and keeps caller streams open. `PrepareUploadAsync` accepts bytes, a single-use +stream, or a factory returning a fresh stream of identical plaintext. Factories +use a hash pass followed by bounded streaming encryption; the SDK owns and +closes each factory stream. It withholds the final bytes until EOF verifies the +second hash. Single-use inputs buffer bounded ciphertext. The default plaintext +limit is 256 MiB. Cancellation does not undo provider-side bytes already sent. + +```csharp +var prepared = await WhatsAppMedia.PrepareUploadAsync( + _ => Task.FromResult(File.OpenRead("attachment.jpg")), + new(WhatsAppUploadMediaType.Image) { Mimetype = "image/jpeg" }); +var uploaded = await client.WhatsAppMedia.UploadRelayAsync("support", prepared); +var descriptor = WhatsAppMedia.ToSendDescriptor(uploaded.Info, uploaded.Location); +await client.Messages.SendAsync("support", + new(new(PhoneNumber: "+15551234567"), new DescriptorImageContent(descriptor))); +``` + +Relay sends ciphertext only through the session's connection. `UploadDirectAsync` +uses a short-lived grant and a separate HTTPS client confined to WhatsApp media +hosts. It preserves grant parameters, refuses redirects, refreshes unauthorized +grants, resumes interrupted uploads and sends documents above 50 MiB in parts. +`WhatsAppDirectUploadOptions` configures the separate CDN proxy/trust settings. +No changed source can reach multipart finalization. + +Standard `DescriptorImageContent`, `DescriptorVideoContent`, +`DescriptorFileContent`, `DescriptorVoiceContent` and `StickerContent` send the +media key through the API. For zero-knowledge sessions, pass +`descriptor.ToSignalDescriptor(...)` to the local `SignalClient` instead. +That conversion rejects thumbnail, waveform and animation metadata because the +pinned service does not accept them. + +`HistoryChunks.DownloadAsync` authenticates encrypted history before decompression +and protobuf parsing. It accepts descriptors or inline payloads, detects +zlib/gzip/raw DEFLATE, and limits inflated data to 512 MiB by default. +Each payload must contain exactly one complete compressed stream. Truncation, +whole-byte tails, padding bytes, concatenated streams, invalid wrapper headers +and checksum failures return `media_invalid_ciphertext`. Unused bits in the last +required DEFLATE byte are allowed. Gzip optional fields, including long terminated +names/comments and FHCRC, are supported; zlib preset dictionaries are refused. +Inflated overflow returns `media_too_large` before a later bad wrapper trailer. + +Decoding snapshots input before its first suspension and validates a full raw +DEFLATE pass plus a last-byte-shortened pass over that same snapshot. Output is +withheld until both passes and wrapper checks finish. Input/output work uses +bounded windows and cooperative cancellation/yields. Cancellation cannot +interrupt a synchronous native decoder or protobuf call already executing. +The default ceiling remains 512 MiB: the compressed snapshot, retained output, +contiguous result and protobuf copies can coexist. This is not zero-copy decoding. +Local exact-stream regressions cover .NET 8.0.3 and 10.0.2; other runtime/platform +acceptance remains separate from these checks. + +Common fields have native types; original protobuf bytes, unknown fields and +`ulong` values remain in `HistoryChunk.Proto`. `DecodeWith` accepts your +application's full-schema decoder. Native history typing covers fewer fields +than TypeScript's complete generated schema. + +`SignalClient` is server-only and uses a separate application bearer secret of +at least 16 visible ASCII characters; values beginning with `pmfa_` throw +`PolymorfaConfigurationException` before any request. +It rejects browser execution, sends no Origin header, refuses redirects, and +never holds Signal master/session keys. Native SSE has no resume cursor or +automatic acknowledgement. Commit application data before `AcknowledgeAsync`; +delivery can repeat. `VerifyWebhook` authenticates the exact timestamp and raw +body before parsing. Its key must be an independent secret of at least 16 bytes; +keys beginning with `pmfa_` throw `PolymorfaConfigurationException` before any +verification. Use a timestamp window and durable event-ID deduplication +before returning 2xx. `SignalHistoryDescriptor.ChunkInput` converts local history +notifications into verified download inputs. + +### Customer-owned storage + +Implement `IWhatsAppMediaUploadTransport` for application-owned uploads and pass +it to `WhatsAppMedia.UploadAsync(session, prepared, transport, cancellation)`. +Its `WhatsAppEncryptedUploadBody` exposes media type, encrypted SHA-256, encrypted +length and `OpenAsync(offset)`. It exposes no media key, plaintext or API +credential. Hash getters return copies, and the returned upload information is +captured before calling the custom transport. + +`RelayTransport()` and `DirectTransport(options)` implement that same interface +and can be wrapped to add application behavior. Existing `UploadRelayAsync` and +`UploadDirectAsync` remain available. Custom names appear in the result's +`Egress`. Custom transports own network requests and opened streams; pass through +cancellation, close streams and finalize only after the verified stream ends. +Changed-source integrity errors and caller transport errors remain unchanged. +Pre-cancellation prevents transport invocation; in-progress cancellation returns +control to the caller. Custom network code must observe its cancellation token. + +`Polymorfa.Sdk.Storage` provides schema-2 operations, deterministic per-field +merges, conditional checkpoints, durable session leases and a memory adapter. +`D1BridgeStorageAdapter.ConnectAsync` verifies a signed handshake with a +customer-hosted D1 Worker binding before returning an atomic adapter. Read the +expected authority and table prefix from trusted application configuration and +use an independent customer application secret. Never use a Polymorfa API +credential for the bridge. HTTPS is required outside loopback fixtures, and +redirects are refused. + +```csharp +using Polymorfa.Sdk.Storage; +using var adapter = await D1BridgeStorageAdapter.ConnectAsync(new( + new Uri("https://storage.example.com/"), "customer-app", "app_", + Environment.GetEnvironmentVariable("CUSTOMER_STORAGE_SECRET")!)); +var storage = new CustomerStorage(adapter); +``` + +The bridge protocol is `polymorfa-d1-storage-v1`, schema 2, source +`9ae35e671f7f4868a67c1562422c5af9284b95c1`. Every response must match the +request nonce, valid exact-body HMAC, expected authority, table prefix and schema. +Commits contain at most 50 writes, including the engine's session fence; reads +and queries are paged at 1,000 rows. Request and response bodies are limited to +8 MiB. Schema setup requires explicit operator enablement in the Worker. +The adapter does not use D1 administrative REST or assume its batches are atomic. + +Large batches span fenced commits, with the deduplication marker committed last. +A failed chunk stops before later rows or the marker. Timeouts and cancellation +can leave a commit outcome unknown. Resume through conditional revisions and +saved event/checkpoint state. Persist content, then the conditional checkpoint, +then acknowledge the application event. Storage never enables hosted history or +automatically acknowledges Signal events. + +The schema/merge baseline is storage source +`3bacf46f9c4213426314001586f5bae43aaa77de`; the native engine additionally +stops after the first failed CAS chunk. Memory and the D1 bridge are included in +the base SDK. Other provider adapters and full event/history reducers remain in +progress. This package remains unpublished. + + +Chat clears and row planning renew the live lease conditionally between reads once +half its duration remains. Expired or replaced leases fail closed. Configure +customer operation deadlines and lease duration so one query or commit can +finish before expiry. + +The separate, unpublished `Polymorfa.Storage.Sql` package adds +`Polymorfa.Storage.Sql.SqlStorageAdapter`. It uses maintained native ADO.NET +SQLite, Npgsql and MySqlConnector drivers without adding database dependencies to +the base SDK. `SqliteAsync`, `Postgres` and `MySql` accept customer database +connection strings. They do not read Polymorfa database settings. Configure TLS +and customer database roles explicitly; a table prefix is naming, not tenant +authorization. PostgreSQL also accepts an explicit schema. Set +`SqlStorageOptions.CreateTables = false` for operator-managed tables and export +schema-2 DDL with `StorageSqlSchema.Statements`. + +```csharp +using Polymorfa.Storage.Sql; +await using var adapter = SqlStorageAdapter.Postgres( + Environment.GetEnvironmentVariable("CUSTOMER_DATABASE_CONNECTION")!, + new() { TablePrefix = "app_", CreateTables = false }); +var storage = new Polymorfa.Sdk.Storage.CustomerStorage(adapter); +``` + +Every SQL commit uses a database transaction and rolls back all writes after a +stale revision. Commit capacities are 500 writes for SQLite and 1,000 for +PostgreSQL/MySQL, including the engine's session fence. Scope and cursor indexes +follow schema 2. Invalid or oversized derived identifiers fail before connecting, +preventing PostgreSQL name truncation. Driver errors omit connection strings, SQL +and customer values. SQL adapters require a server application. Hosted +Supabase/Neon/PlanetScale authority and deployment acceptance remain customer +configuration and verification gates. The browser-local IndexedDB adapter and a +Supabase RLS preset are described below. + + +The separate, unpublished `Polymorfa.Storage.S3` package adds +`Polymorfa.Storage.S3.S3MediaStore` using the official AWS S3 driver. Pass a +customer-owned `IAmazonS3` client with explicit credentials, endpoint, TLS and +retry settings. The helper borrows that client and does not dispose it or copy +Polymorfa API credentials into it. Storage and media operations require a server +application. + +```csharp +using Polymorfa.Storage.S3; +var store = new S3MediaStore(new(customerS3Client, "customer-media", "app/")); +var archived = await store.ArchiveAsync(descriptor, + new("incoming/photo", customerPrivateSpoolDirectory)); +var uploaded = await store.UploadToWhatsAppAsync( + new(messaging.WhatsAppMedia, sessionId, "incoming/photo", + Polymorfa.Sdk.WhatsAppUploadMediaType.Image)); +``` + +`ArchiveAsync` accepts a decoded descriptor or inbound base64 media and message +type fields. It snapshots mutable descriptor bytes before I/O and requires a +caller-selected private spool parent. It verifies the MAC, padding and plaintext hash, plus the ciphertext hash and +exact length when supplied, before starting any object PUT, then removes its +private spool. A corrupt, truncated or +cancelled download cannot overwrite an existing object. Metadata contains only +the selected media kind, plaintext hash, optional encoded filename and explicit +caller metadata; the helper never derives metadata from the media key. + +`UploadToWhatsAppAsync` uses two streaming reads pinned to the same object ETag. +A failed conditional GET or a changed complete second-pass hash fails before +final encrypted bytes or finalization. Relay is the default; select +`S3WhatsAppEgress.Direct` to use grant-based direct upload. Object writes can +have an uncertain outcome after cancellation or transport failure. + +The `S3MediaStore.UploadToWhatsAppAsync(options, transport, cancellation)` +overload accepts a custom `IWhatsAppMediaUploadTransport` and retains both +ETag-pinned source reads. An independent custom transport makes no Polymorfa API +request; built-in relay/direct transports can also be wrapped. + +This media helper follows source +`254fc6a2d073e7fc79420cf95f9b03927d00c04a`. Native tests exercise the official +driver against MinIO, including verified spooling, conditional reads, changed +objects, cancellation and ciphertext-only relay/direct uploads. Hosted AWS/R2 +credentials, IAM policies and deployment behavior remain separate acceptance +gates. This package has not been published. + + +`Polymorfa.Storage.Sql.SupabaseStoragePreset.Sql` exports the schema-2 customer +tables, live session memberships and forced-RLS policies from storage source +`3bacf46f9c4213426314001586f5bae43aaa77de`. Review and install it once in a +customer-owned migration using `SupabasePresetOptions`. Adapter setup never +installs these policies. Reuse the schema and prefix in `SqlStorageOptions` and +set `CreateTables = false` for the server SQL adapter. + +The separate reader role can select only sessions mapped to `auth.uid()` in +`storage_memberships`. It cannot write storage data or memberships. Configure +trusted Supabase/PostgREST JWT enforcement; never expose a direct reader SQL +credential or let a browser set the database subject. Keep reader and server +roles distinct, without superuser/BYPASSRLS rights or reader inheritance of the +writer role. Membership deletion takes effect on the next query. Native +PostgreSQL tests cover all 17 entities, anonymous/write denial, a non-superuser +owner's FORCE-RLS boundary, revocation and missing-subject denial. Hosted +Supabase JWT and deployment acceptance remain separate gates. + + +The separate, unpublished `Polymorfa.Storage.DynamoDb` package adds +`Polymorfa.Storage.DynamoDb.DynamoDbStorageAdapter` through the official AWS +driver. Pass a customer-owned `IAmazonDynamoDB` client and +`new DynamoDbStorageOptions(namespace, tableName)`. The caller configures +credentials, TLS/endpoints, retries and deadlines, and owns client lifetime. +The adapter never reads Polymorfa API credentials and requires a server app. + +The table needs string partition key `pk` and sort key `sk`. Setup validates +these key names. Set `CreateTable = true` only for explicit table creation; AWS +table activation is a separate provisioning gate. Namespaces separate stored +keys; configure customer IAM/table authority independently. + +Conditional transactions contain at most 100 writes, including the session +fence. Strongly consistent queries page at 1,000 rows and scan the entity +partition before applying its schema index. `MaxQueryRows` defaults to 100,000 +for the complete scanned partition. DynamoDB item and transaction byte limits +still apply. Timeout and transport failures can leave a commit outcome unknown; +resume with saved event/checkpoint state and conditional revisions. + +The storage source is `3bacf46f9c4213426314001586f5bae43aaa77de`. DynamoDB +Local tests use the official driver and cover all 17 entities, JSON scalars, +atomic rollback, 2,000-message history, replay, cursor paging, checkpoints, +expired-writer fences and caller-mutation capture. Hosted AWS credentials, IAM +and deployment acceptance remain separate gates. + + +The separate, unpublished `Polymorfa.Storage.MongoDb` package adds +`Polymorfa.Storage.MongoDb.MongoDbStorageAdapter` through the official native +MongoDB driver. Pass a customer-owned `IMongoDatabase` and +`new MongoDbStorageOptions(namespace)`. The caller configures credentials, TLS, +server selection, connection and operation timeouts and owns client lifetime. +The adapter derives sessions from that database's client, never reads Polymorfa +API credentials and requires a server app. + +```csharp +using Polymorfa.Storage.MongoDb; +var adapter = new MongoDbStorageAdapter(customerDatabase, + new MongoDbStorageOptions("my_app")); +``` + +Use a replica set or sharded deployment with transaction support. Setup refuses +standalone servers before index changes and creates schema-2 indexes by +default. Set `CreateIndexes = false` for operator-managed indexes. Collections +are named `polymorfa_{namespace}_{entity}`; configure database permissions +separately from the namespace. + +Commits use native snapshot/majority transactions and at most 500 writes, +including the session fence. Conditional revisions and the native driver's +transaction retries preserve atomicity. MongoDB document and transaction +lifetime limits still apply. A transport failure or timeout may leave a commit +outcome unknown; recover using saved event/checkpoint state and conditional +revisions. + +Queries forward only declared index scope fields. `MaxQueryRows` defaults to +100,000 for the complete matching scope before cursor/range sorting. Bound +retention or configure the limit for the workload. JSON remains JSON, including +literal `$date`, `$binary` and `$numberLong` objects; BSON-specific stored types +are refused. Caller row and query values are captured before asynchronous work. + +The storage source is `3bacf46f9c4213426314001586f5bae43aaa77de`. +Actual local replica-set tests cover all 17 entities, literal/scalar JSON, +2,000-message history, atomic rollback, replay, checkpoints, expired-writer +fences, namespaces, read bounds and mutation during a request. Actual standalone +tests prove refusal without partial writes. Hosted MongoDB authorization, TLS, +sharded deployment and operational failover remain customer acceptance gates. + + +The separate, unpublished `Polymorfa.Storage.Cosmos` package adds +`Polymorfa.Storage.Cosmos.CosmosStorageAdapter` using the official Azure Cosmos +DB .NET driver. Pass a customer-owned `Container` and +`new CosmosStorageOptions(namespace)`. The caller configures credentials, +TLS, endpoints, connection mode, retry policy and timeouts and owns the client. +The adapter uses native stream APIs for its JSON wire shape, independently of +any custom serializer on that client. It never reads Polymorfa API credentials +and requires a server app. + +```csharp +using Polymorfa.Storage.Cosmos; +var adapter = new CosmosStorageAdapter(customerContainer, + new CosmosStorageOptions("my_app")); +await adapter.SetupAsync(); +``` + +Provision the customer database and container separately. The container must +have exactly one partition-key path, `/namespace`; setup verifies it and never +creates resources. Every commit stays in that namespace's logical partition. +Namespace keys complement customer account/container permissions and do not +replace them. + +Conditional commits use native atomic batches with at most 100 writes, +including the session fence. Each replacement carries the ETag from its point +read, so a concurrent writer rolls back the complete batch. Deletes replace +payloads with tombstones to preserve conditional identity. Tombstones consume +customer storage; this adapter does not silently purge them. Cosmos document, +batch, logical-partition and throughput limits still apply. A timeout can leave +a write outcome unknown; resume with saved event/checkpoint state and +conditional revisions. + +Queries use fixed, parameterized SQL and scan the namespace's entity rows +before applying the schema index, range and cursor. `MaxQueryRows` defaults to +100,000 for that scan; each query response is bounded to 8 MiB. Bound customer +retention and configure operation deadlines and lease duration for the workload. +Caller rows and query scopes are captured before asynchronous work. + +The schema source is `3bacf46f9c4213426314001586f5bae43aaa77de`. +Actual local Cosmos DB emulator tests use the official driver and cover all +17 entities, literal/scalar JSON, 2,000-message history, replay, checkpoints, +expired-writer fences, tombstones, namespace/read bounds, incorrect partition +keys and caller mutation. A forced concurrent replacement proves native +ETag-batch rollback with no partial insert. Hosted Azure credentials, account +permissions, TLS, region consistency and operational failover remain customer +acceptance gates. + +### Customer-owned Firestore + +The separate, unpublished `Polymorfa.Storage.Firestore` package adds +`Polymorfa.Storage.Firestore.FirestoreStorageAdapter` through the official +`Google.Cloud.Firestore` 4.5.0 client. Pass a customer-owned `FirestoreDb` and +`new FirestoreStorageOptions(namespace)`. The caller owns project/database +selection, Google credentials, TLS, deadlines and client lifetime. Database +credentials stay separate from Polymorfa API credentials. This server adapter +refuses browser construction. + +```csharp +using Polymorfa.Storage.Firestore; +var adapter = new FirestoreStorageAdapter(customerDatabase, + new FirestoreStorageOptions("my_app")); +``` + +Collections use `polymorfa_{namespace}_{entity}`. Native transactions read all +touched documents before writing, check row revisions and commit at most 250 +writes, including the engine lease fence. A stale revision rejects the complete +batch. Inputs are copied before the first asynchronous call, so retries retain +the prepared row and query scope. Queries use declared equality fields only, +read at most `MaxQueryRows + 1` rows (default 100,001), reject an oversized scan +and apply portable range, cursor and ordering rules. `SetupAsync` does not +provision collections or indexes. The customer configures required composite +indexes and retention. Firestore document, nesting, field-name and transaction +limits still apply. + +This implements source `3bacf46f9c4213426314001586f5bae43aaa77de`. Literal +JSON tag-shaped objects remain ordinary objects; Firestore-native timestamps, +references, blobs and other non-JSON values are refused. Actual emulator tests +use the official driver for all 17 schema entities, JSON values, 2,000 history +rows, rollback, replay, checkpoint CAS, lease fencing, namespace separation, +closed query scopes and scan bounds. Hosted Google IAM, TLS, indexes and +production availability remain separate verification gates. + +### libSQL and Turso storage + +The separate, unpublished `Polymorfa.Storage.LibSql` package adds +`LibSqlStorageAdapter` using Turso's maintained +`Turso.Data.Sqlite.Provider` 0.8.3. It reuses `Polymorfa.Storage.Sql` and its +schema-2 DDL, typed rows and revision checks. Install the optional package only +in server applications; the base SDK gains no database dependency. This package +also includes the existing SQL package's SQLite, PostgreSQL and MySQL drivers. +The storage source is `3bacf46f9c4213426314001586f5bae43aaa77de`. + +```csharp +using Polymorfa.Storage.LibSql; +using Polymorfa.Storage.Sql; +using Turso.Data.Sqlite; + +await using var connection = new SqliteConnection( + Environment.GetEnvironmentVariable("CUSTOMER_LIBSQL_CONNECTION")!); +await connection.OpenAsync(); +await using var adapter = new LibSqlStorageAdapter(connection, + new SqlStorageOptions { TablePrefix = "app_" }); +await adapter.SetupAsync(); +var storage = new Polymorfa.Sdk.Storage.CustomerStorage(adapter); +``` + +This adapter supports direct remote Hrana connections only. Native files, +in-memory databases and embedded replicas are rejected before adapter commands. +Use `Polymorfa.Storage.Sql.SqlStorageAdapter.SqliteAsync` for local SQLite. +Turso provider 0.8.3 exhibited intermittent native reader and lock failures in +repeated independent-file-writer tests, including with pooling enabled. The adapter does not reopen or replace the caller's connection. + +The caller opens and owns the remote connection and configures its database +URL, auth token, TLS and timeouts. Use a server-held customer database token with the +required database permissions. Pass no Polymorfa API credential to this driver. +Keep connection strings out of browser bundles, logs and exception reports from +application-owned connection setup. Use the provider's connection-string +builder when composing values. Direct remote connections require TLS; +`libsql://` uses HTTPS by default. +Loopback development can use `Tls=False` or HTTP. +A table prefix provides naming, not tenant authorization. A customer database +and its access policy remain the customer's responsibility. + +The adapter borrows the open connection, serializes its operations and leaves +it open when disposed. Give each adapter an exclusive connection; application +commands and another adapter must not share that connection while it is in use. +Each commit uses an explicit database transaction, including conditional deletes +and inserts. A revision or duplicate-key conflict rolls back the entire commit. +The 500-write limit includes the engine's session fence. A provider batch alone +does not establish atomicity. Driver failures surfaced by adapter operations +omit raw errors, inner exceptions, URLs, SQL and stored values. Cancellation +retains a sanitized `OperationCanceledException`. Network failures during commit +can leave its outcome uncertain; inspect the saved checkpoint before resuming. +Unclassified transaction admission failures return a sanitized provider failure +rather than an assumed revision conflict. A fresh retry still checks the original +expected revision. The adapter does not silently retry an uncertain commit. + +`CreateTables = false` supports customer-managed schema installation. Export +SQLite schema-2 statements with `StorageSqlSchema.Statements` and +`StorageSqlDialect.Sqlite`. Direct remote Hrana connections are exercised by +the adapter tests. Replica sync, failover and production Turso credentials are +not acceptance-tested. This source package is not published and does not establish hosted service availability or a feature release. + +Run its direct remote conformance executable with: + +```bash +dotnet run --project packages/dotnet/tests/Polymorfa.Storage.LibSql.Tests +``` + +Set `POLYMORFA_STORAGE_LIBSQL_CONNECTION` to a customer-owned test connection +string to include direct remote conformance. +Set `POLYMORFA_STORAGE_LIBSQL_RACE_ITERATIONS` (1-1000, default 20) for repeated +independent-client races. The tests use unique table +prefixes and retain remote test tables. They cover all 17 entities, JSON/scalar +round trips, 2,000-message history, paging, replay, lease fencing, stale +checkpoints, revision and duplicate-key rollback, validation, cancellation, +sanitized errors and borrowed connection lifetime. Independent remote connections +race on one revision and verify exactly one winner; an unclassified admission +failure remains explicit. Removing the prepared-input snapshot makes the +mutation test fail. + +### Entity Framework Core models + +The separate, unpublished `Polymorfa.Storage.EntityFrameworkCore` package exports +schema-2 models for all 17 customer-owned SQL entities. It uses EF Core 8.0.32 +and the SQL package's existing drivers. This configures models only; it does +not connect to a database, install tables or change access policies. + +Use `PolymorfaStorageDbContext` with application-owned provider options and the +same `StorageSqlDialect` and `SqlStorageOptions` as the native SQL adapter. +Each entity is an EF shared-type property bag. Property names follow the +portable schema (`sessionId`, `messageId`, `timestamp`); physical column names +match the native adapter (`session_id`, `message_id`, `timestamp`). JSON +properties use `JsonElement?`, `_v` uses `JsonElement`, and `_rev` is a `long` +concurrency token. Optional scalar properties are nullable. + +```csharp +using Microsoft.EntityFrameworkCore; +using Polymorfa.Sdk.Storage; +using Polymorfa.Storage.EntityFrameworkCore; +using Polymorfa.Storage.Sql; + +await using var context = new PolymorfaStorageDbContext( + customerEfOptions, StorageSqlDialect.Postgres, + new SqlStorageOptions { TablePrefix = "app_" }); +var recent = await context.StorageRows(StorageEntity.Messages).AsNoTracking() + .Where(row => EF.Property(row, "sessionId") == sessionId) + .OrderByDescending(row => EF.Property(row, "timestamp")) + .Take(50).ToListAsync(); +``` + +The supplied context includes dialect, schema and table prefix in its model +cache key. Existing application contexts can call +`modelBuilder.ConfigurePolymorfaStorage(dialect, options)` and use +`context.StorageRows(entity)`; configure one storage prefix per model and +include naming options in the application's model cache key if they vary. +Identifiers and derived index names receive the native SQL schema validation. +PostgreSQL schema selection and binary key collations match the SQL adapter. +The customer owns migrations, provider configuration, database credentials and +live access policies. A table prefix does not grant tenant authority. + +Use `CustomerStorage` for event/history writes: EF's revision concurrency token +does not apply the storage engine's field-version merges, replay markers, +checkpoint CAS or lease fences. Direct application writes must respect those +rules. Model JSON converters preserve ordinary JSON, including literal +`$date`/`$binary` objects, and snapshot parsed values independently of a +`JsonDocument` lifetime. + +This follows source `3bacf46f9c4213426314001586f5bae43aaa77de`. Actual SQLite, +PostgreSQL and MySQL checks prove all 17 native-table mappings, nullable values, +JSON replacement, stale EF revisions and model-cache separation. An EF-created +SQLite schema also passes the native engine's complete conformance suite, +including 2,000 history rows and fenced writes. Hosted database access and +application migrations remain customer acceptance gates. + +### Store events and decoded history + +`StorageReducers.ReduceEvent` maps the 24 stored webhook event types to schema-2 operations. `ReduceHistoryChunk` accepts `StorageHistoryInput` and typed `StorageHistoryChunk` objects from a full WhatsApp protobuf decoder. It maps messages, media references, receipts, reactions, group membership, push names, PN/LID mappings and history progress. An explicit `StorageHistoryIdentityIndex` joins native identifiers to Polymorfa IDs; unmatched identifiers use stable `wa:` keys. Revokes and deletes permanently clear the contract's content and media secret fields. Other event types produce empty batches. + +The HTTP history reader preserves raw protobuf bytes and decodes its documented subset. Supply a full-schema decoder to populate the storage history models. The storage reducer does not infer missing protobuf fields or download media bytes. + +```csharp +using Polymorfa.Sdk.Storage; + +var storage = new CustomerStorage(customerOwnedAdapter); +var result = await storage.ApplyEventAsync(verifiedWebhook); +// Return a successful webhook response after this task completes. + +await using var sync = platform.Project(projectId).Events.SyncToStorage(storage); +await sync.Completion; +``` + +`ApplyEventAsync` also accepts `StorageApplyEventOptions.DecodeHistory` for linked `history.sync` notifications. The callback receives the typed notification and verified event, and returns a fully decoded `StorageHistoryChunk` or `null` for progress only. Replay checks the stored event ID before invoking the callback. Cloud inline history notifications do not invoke it. `storage.WebhookHandler(options)` returns the corresponding async webhook handler; verify the original body signature before calling it. + +`SyncToStorage` requests manual acknowledgement and resumes the saved cursor. It stores the reduced event and deduplication marker, saves the cursor with a compare-and-set guard, then acknowledges the stream. The default checkpoint is session `*`, stream `events`. Missing event bodies and retention gaps stop consumption without advancing the checkpoint or acknowledgement. Set `RequirePayload = false` only for intentional metadata-only consumption. `OnApplied` runs after acknowledgement; a failure there leaves the event and cursor stored. + +`Stop()`, disposal and cancellation stop admission of the next event. An event already admitted finishes storage, checkpoint and acknowledgement with independent operation lifetimes. Await `Completion` or `DisposeAsync` to observe its outcome. Stopping the stream does not hide failures from an admitted storage or acknowledgement operation. Adapter, decoder, checkpoint and acknowledgement failures propagate; an unacknowledged event can replay and deduplicate. `IStorageEventStreamSource` allows an application-owned stream with the same persistence ordering. These helpers acknowledge the Platform event stream; they do not change any local Signal service acknowledgement. + +This source is unpublished. The SDK's existing server browser-credential restriction and the API's permissions and availability checks still apply. + +### Blazor WebAssembly IndexedDB storage + +The separate, unpublished `Polymorfa.Storage.IndexedDb` package adds +`IndexedDbStorageAdapter` for browser-local schema-2 storage. It uses the +application's `IJSRuntime` and a packaged JavaScript module with native +IndexedDB transactions. The source contract is +`3bacf46f9c4213426314001586f5bae43aaa77de`. The base SDK gains no browser runtime +or database dependency. + +Call it after Blazor's JavaScript runtime is available, such as from a +component's first `OnAfterRenderAsync` callback: + +```csharp +using Polymorfa.Storage.IndexedDb; +using Polymorfa.Sdk.Storage; + +await using var adapter = await IndexedDbStorageAdapter.ConnectAsync(JS, + new IndexedDbStorageOptions("user_42") + { + DatabaseName = "customer_app_user_42", + MaxQueryRows = 100000 + }); +var storage = new CustomerStorage(adapter); +``` + +The adapter borrows the runtime and owns its module reference and database +connection. Dispose it when the component or application owner ends. Disposal +closes the connection and retains the stored data. No Polymorfa API client, +credential, hosted retention setting or network storage service is involved. +The SDK's browser prohibition on organization and project credentials remains +in force; browser API calls still require an authorized client principal. + +Choose a stable, separate namespace and database for each application owner. +The namespace must start with an ASCII letter and contain at most 64 letters, +digits, underscores or hyphens. A database is bound to one namespace, including +when callers choose its name explicitly. This prevents accidental rebinding; +it is not authentication, encryption or isolation from other scripts on the +same origin. The application owns login/logout handling, account changes, +retention and deletion of its device data. Browser storage can be denied, +limited or evicted; do not treat it as a guaranteed permanent archive. + +All 17 entity stores use schema version 2. Setup upgrades an existing version-1 +database without discarding its existing rows. An upgrade blocked by an older +open connection returns a safe provider error; close that connection and retry. +Native version changes close the old connection. Unsupported newer database +versions fail to open. Index names and scopes come from the SDK's closed schema. +Queries scan the selected entity within `MaxQueryRows` (1-100000, default +100000); exceeding that count fails before loading its rows. This adapter does +not create native secondary indexes or claim unbounded query performance. + +Each commit supports at most 1000 writes, including the engine's session fence. +The module reads every expected revision and queues every write in one native +read/write transaction. It returns success only after the transaction's +`complete` event. A conflict or confirmed abort leaves the entire commit +unapplied. Large history batches use the existing engine's fenced commits and +write their replay marker last. + +Cancellation before admission starts no operation. Cancellation during an +active transaction requests a native abort and waits for its final receipt. +If completion wins the race, the committed operation returns success. A canceled +.NET wait alone never establishes rollback. Runtime interruption can leave a +commit outcome unknown; inspect its saved checkpoint before resuming. Native +errors returned by adapter operations omit raw browser exceptions, customer rows +and inner exceptions. + +The committed Blazor fixture runs the real C# engine and IndexedDB in Chromium. +It covers all 17 entities, JSON/scalars, 2000-message history, pagination, +conditional rollback, replay, stale checkpoints, lease fences, namespace binding, +query bounds, caller mutation, active-transaction cancellation, cancellation +after commit, schema upgrades, blocked-upgrade recovery and browser credential +guards. The pinned TypeScript oracle covers 43 event and 15 history reductions; +native row snapshots verify event/history persistence and replay. A synthetic +full-schema decoder and application-owned event stream exercise decoded history +and persistence/checkpoint/acknowledgement ordering without API credentials. +A sandbox with an opaque origin exercises native storage denial. +Safari, Firefox, browser crash recovery, device storage persistence permissions +and production application integration remain acceptance work. This package +has not been published and introduces no hosted feature release or entitlement. + +Build the fixture outside the repository and serve its static files on loopback: + +```bash +dotnet publish packages/dotnet/tests/Polymorfa.Storage.IndexedDb.BrowserTests \ + -c Release -p:PublishTrimmed=false \ + -o "$HOME/.polymorfa-agent-work/dotnet-indexeddb/browser" +python3 packages/dotnet/tests/Polymorfa.Storage.IndexedDb.BrowserTests/serve.py \ + --root "$HOME/.polymorfa-agent-work/dotnet-indexeddb/browser/wwwroot" +``` + +Open `http://127.0.0.1:54865/?id=unique-test-run`. Each run needs a fresh `id`; +append `&reopen=1` to the same URL to verify durable reads after a page reload. +The fixture uses synthetic data and does not access an operator browser profile. + +## Campaign and direct-history contracts + +Campaign requests expose `CampaignVariableMapping`, `CampaignVariableBinding` +and `CampaignTrackedLink`. Use `PatchValue.Set(null)` for a PATCH reset; +leave its default value to omit the field. Create mappings and binding members +reject explicit null. Messaging updates require at least one captured field. +Platform updates allow null or empty bodies and open extensions, but reject +`projectId`, `id` and extensions that collide with typed fields. Select the +Project in `PlatformCampaignParameters` and the campaign in the method path. +Platform launch takes `LaunchPlatformCampaignRequest` and returns a typed, +open `PlatformCampaignLaunchResult`. + +The client checks mapping, link, variation and follow-up constraints before HTTP. +Tracked-link checks validate URL syntax and domain names, without DNS lookup. +Requests retain the caller's URL spelling; returned URLs reflect server storage. +Follow-up validation uses the server's effective defaults without adding omitted +timing fields to the request. Stored link arrays have no request-count cap. +Platform composed messages use `PlatformCampaignMessage`; empty drafts and open +nested media/button extensions remain supported. Stored campaign `Messages` +remain opaque JSON. Recipient filters can combine reason, status, cursor and +limit. Messaging recipient `NextMessageAt` retains fractional JSON numbers; +Platform recipient decoding requires an integer. + +Read history delivery with `HistorySyncDelivery` (`Archive` or `Descriptor`). +Writes use `HistorySyncDeliveryOverride.Descriptor`. Delivery-specific writes +and resets, initial QuickLink custody, and QuickLink delivery select +`2026-10-14` only when the request has no explicit `ApiVersion`. Existing GETs, +whole-history resets and requests without new intent retain the client default. +`Session.SignalMode` is an optional immutable fact, separate from configuration. +Initial QuickLinks cannot select an existing session. Supplementary links require +both session and connection, and cannot choose custody, Customer, external ID, +billing controls or a single-connection goal. + +Direct grants and history retry/delete require server credentials. Prepared +uploads may already have consumed plaintext during explicit local preparation; +a Client denial prevents further source, grant and storage work. Scoped Client +relay uploads and custom ciphertext-only transports remain supported. A retry +acknowledgement is not chunk delivery. Deletion validates the API acknowledgement +and does not delete local history. + +Verified `history.sync` parsing exposes `DirectHistorySyncMediaPayload` and +`DirectHistorySyncInlinePayload` alongside archive and Cloud events. Descriptor +events use projected WhatsApp identities and ISO time, and require exactly one +media or inline branch. Their diagnostics redact descriptor material. Typed +parsing does not download, decode or persist history. `StorageHistoryDecoder` +remains archive-only. Use the separate `StorageHistoryDescriptorDecoder` and +descriptor handling modes described below for descriptor storage. + +Sticker sends require a WebP descriptor and reject caption and unsupported +media fields before HTTP. A standard descriptor send includes its key in the +API body and does not establish customer-owned Signal custody. Server consent, +eligibility, approval, funding, feature policy and pairing checks still apply. +These source and local package checks do not establish registry publication, +live WhatsApp, browser pairing or production availability. + +### Descriptor history storage + +`StorageApplyEventOptions.DecodeHistory` remains archive-only with the same +`LinkedHistorySyncPayload`, event and cancellation arguments, and nullable +progress-only result. Use the separate descriptor callback: + +```csharp +var handler = storage.WebhookHandler(new() +{ + DecodeHistoryDescriptor = async (descriptor, webhookEvent, cancellation) => + await DownloadDecodeAndProjectAuthorizedHistoryAsync(descriptor, webhookEvent, cancellation) +}); +``` + +The application defines this function. It receives a validated +`DirectHistorySyncPayload` and the original verified event and must return a +non-null `StorageHistoryChunk`. The application owns authorized media access, +key custody, complete-schema decoding and storage projection. `HistoryChunks` +preserves verified protobuf and its documented subset; it does not supply a +generic full-schema decoder. Storage never downloads media implicitly. Missing, +null, failed or invalid descriptor decoding fails before progress, marker, +checkpoint or ACK. A legitimate empty chunk is allowed. Descriptor errors and +diagnostics omit media keys, hashes and inline bytes. + +The default `HistoryDescriptorMode = StorageHistoryDescriptorMode.Decode` +requires the new callback. Explicit `StorageHistoryDescriptorMode.MetadataOnly` +validates and consumes progress and the event marker, skipping all decoders even +when configured. Stream sync then saves its cursor and manually ACKs. The event +is deduplicated, so adding a decoder later cannot import it through ordinary replay. +There is no automatic backfill or marker deletion. Valid duplicate descriptors +skip decoders before checking availability. Notification IDs never map individual +messages; existing native `wa:` fallback keys retain their joining limits. Content +and marker commit precede checkpoint CAS and ACK. A failed remote ACK leaves +committed data intact. Archive and Cloud behavior remain unchanged. This establishes +local package behavior, not registry or deployed availability. + +Decoded-history numeric strings follow the producer's finite `Number(string)` +contract, including signed zero, exact ECMAScript whitespace and full ASCII +numeric grammar. Unsigned radix values retain the existing BigInteger path and +one binary64 rounding. Bare radix prefixes already failed correctly. Generic +row numbers and safe revisions remain strict. Full public assemblies and local +NuGet consumers provide local evidence; registry publication and matching +environment acceptance remain separate release gates. + +Media downloads retain the successful API request deadline through body EOF. Each approved storage redirect hop has its own request deadline, retained through the final body. Async and synchronous body reads distinguish the request deadline from caller cancellation. Disposing a download releases its response, timer and owned storage transport; it does not dispose the API client. Body failures do not replay the download. Calls WebSockets retain the configured base-path prefix and authenticate in the first frame. Raw cursor pages reject repeated cursors before requesting them again, including cycles across manually requested pages. + +Signal finite JSON requests share one header/body deadline. Event streams enforce a requested session and report incomplete lines or frames at EOF; they retain their idle timeout and manual acknowledgment policy. ACK success requires exactly HTTP 204. Usage iteration preserves a starting cursor and rejects its repetition. Native HTTP fixtures verify these local consumer rules; they do not establish service deployment or registry availability. + +## Credential handling + +Treat every Polymorfa credential as an opaque string. Pass the complete value +from the Console. Never parse credentials or assume a fixed length. Store at +least 512 characters, preferably unrestricted text. Client-token length varies +with its bindings, Customer block and signing key ID. + +The SDK checks server-credential prefixes, canonical base64url and the checksum. +These checks do not authenticate a credential. The API checks the issuance record +and live permissions. Test keys use the same project-token format; their test-only +restriction is enforced by the service. + +### Project event-stream validation and limits + +`EventStream.Cursor` advances after the base64 payload, webhook envelope and +stream delivery fields parse. Invalid base64 follows the existing reconnect +policy. An invalid decoded webhook remains terminal; after correcting the source, +resume explicitly from the retained cursor or durable storage checkpoint. +Project event frames have a 4 MiB UTF-8 byte budget, including incomplete lines, +all `data:` lines, field syntax and comments. CRLF counts as one line terminator; +a blank line resets the budget. Oversized frames follow the existing reconnect +policy. Signal parsing is unchanged. + +Storage synchronization uses `StreamOptions.Since` when no checkpoint exists and +the saved cursor otherwise. Event commit, checkpoint CAS and manual ACK keep that +order. These changes are unreleased source behavior. + + +## Official API round-2 betas + +Native Number catalog-cache reads/sync, marketing status, Official group JPEG +upload, identity changes, incoming carts and signed webhook batches have source +counterparts. These additions remain unreleased betas and may return +`feature_unavailable`. They grant no enrollment or provider authority. See +[the shared beta guide](../../docs/official-api-round2.md) for ownership, batching, +whole-body signatures and release gates. + +Incoming cart `CloudOrderItem.Quantity` uses `long` to preserve the API's positive +safe integers through `9007199254740991` in signed deliveries, batches and history. +`ItemPrice` remains `double`; catalog prices remain strings. The JSON contract is +unchanged. Callers assigning a quantity to `int` must check their own range before +converting, and rebuild consumers against the widened DTO. This source correction +is unreleased. + +The following uses an existing server client. Preserve the API's opaque cursor; +stop when the response omits it. Catalog prices remain exact strings. + +```csharp +var page = await client.Catalogs.ListProductsAsync("number-id", "123", new(100, "opaque_-"), new() { CancellationToken = cancellationToken }); +var nextCursor = page.Data.Data.NextCursor; +``` + +## Simultaneous calls source preparation + +This counterpart is prepared against API #559 merge `c1a43a1e`. It is not a +registry availability claim. Use the HTTP API until a matching SDK release is +published and accepted against the target environment. + +Call-settings reads expose `simultaneousCalls`, default false. Updates accept +`simultaneousCalls` and `acknowledgeRisk`. Omission preserves stored state; +explicit false disables the setting. Acknowledgment alone changes no setting +and is rejected. The API requires a team key and `acknowledgeRisk: true` for an +off-to-on transition. Project tokens may disable it. The SDK forwards values +without guessing the stored state. Existing `invalid_parameter`, +`permission_denied` and `state_conflict` errors remain authoritative. + +The setting affects linked-device Numbers. Cloud API Numbers ignore it. +Unlimited concurrent calls differ from WhatsApp's apps and void Ban Insurance +when the 30-day claim window overlaps an enabled period. Claim verdicts and +`bansafe.claim` webhook verdicts recognize `simultaneous_calls`. Claim evidence +includes `simultaneousCalls`; the webhook does not acquire an evidence object. + +No browser/client-token authority, call media behavior, automatic state read, +retry policy or feature availability is added by these SDK fields. diff --git a/packages/dotnet/coverage.json b/packages/dotnet/coverage.json new file mode 100644 index 00000000..3c5dc7f3 --- /dev/null +++ b/packages/dotnet/coverage.json @@ -0,0 +1,4514 @@ +{ + "schemaVersion": 1, + "sourceSdkCommit": "e24cb176fc1124f5fe852ff6c77902648fd13b4a", + "operations": [ + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/business/collections/{collectionId}", + "status": "covered", + "sdkMethod": "MessagingClient.Business.DeleteCollectionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/business/products/{productId}", + "status": "covered", + "sdkMethod": "MessagingClient.Business.DeleteProductAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/business/profile/cover-photo/{coverPhotoId}", + "status": "covered", + "sdkMethod": "MessagingClient.Business.DeleteCoverPhotoAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/business/quick-replies/{quickReplyId}", + "status": "covered", + "sdkMethod": "MessagingClient.QuickReplies.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/channels/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/chats/{conversation}/messages/{messageId}", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.DeleteMessageAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/groups/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/labels/{labelId}", + "status": "covered", + "sdkMethod": "MessagingClient.Labels.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/official-groups/{group}", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/profile/picture", + "status": "covered", + "sdkMethod": "MessagingClient.Profile.DeletePictureAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/{session}/templates/{name}", + "status": "covered", + "sdkMethod": "MessagingClient.CloudTemplates.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/projects/{projectSlug}/templates/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Templates.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/quicklinks/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.QuickLinks.CancelAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/voip/calls/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.EndAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/messaging/webhooks/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Webhooks.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/health", + "status": "covered", + "sdkMethod": "SystemClient.HealthAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SystemTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/catalog", + "status": "covered", + "sdkMethod": "MessagingClient.Business.GetCatalogAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/collections", + "status": "covered", + "sdkMethod": "MessagingClient.Business.ListCollectionsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/collections/{collectionId}", + "status": "covered", + "sdkMethod": "MessagingClient.Business.GetCollectionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/compliance", + "status": "covered", + "sdkMethod": "MessagingClient.Business.GetMerchantComplianceAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/eligibility", + "status": "covered", + "sdkMethod": "MessagingClient.Business.GetEligibilityAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/linked-accounts", + "status": "covered", + "sdkMethod": "MessagingClient.Business.GetLinkedAccountsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/products/{productId}", + "status": "covered", + "sdkMethod": "MessagingClient.Business.GetProductAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/profile", + "status": "covered", + "sdkMethod": "MessagingClient.Business.GetProfileAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/business/quick-replies", + "status": "covered", + "sdkMethod": "MessagingClient.QuickReplies.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/call-permissions/{to}", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.RetrieveCallPermissionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/channels", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/channels/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/channels/{id}/message-updates", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.ListMessageUpdatesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/channels/{id}/messages", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.ListMessagesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/chats", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/chats/{conversation}", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/chats/{conversation}/messages", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.ListMessagesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/chats/{conversation}/messages/{messageId}", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.RetrieveMessageAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/chats/{conversation}/messages/{messageId}/media", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.DownloadMessageMediaAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MediaApiTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/chats/{conversation}/service-window", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.GetServiceWindowAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/cloud-credentials", + "status": "covered", + "sdkMethod": "MessagingClient.Sessions.GetCloudCredentialHealthAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/contacts", + "status": "covered", + "sdkMethod": "MessagingClient.Contacts.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/contacts/{contactId}", + "status": "covered", + "sdkMethod": "MessagingClient.Contacts.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/contacts/{contactId}/business-profile", + "status": "covered", + "sdkMethod": "MessagingClient.Contacts.BusinessProfileAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/contacts/{contactId}/devices", + "status": "covered", + "sdkMethod": "MessagingClient.Contacts.DevicesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/contacts/{contactId}/info", + "status": "covered", + "sdkMethod": "MessagingClient.Contacts.InfoAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/contacts/{contactId}/picture", + "status": "covered", + "sdkMethod": "MessagingClient.Contacts.PictureAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/contacts/blocked", + "status": "covered", + "sdkMethod": "MessagingClient.Contacts.BlocklistAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/contacts/check", + "status": "covered", + "sdkMethod": "MessagingClient.Contacts.CheckAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/groups", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/groups/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/groups/{id}/invite-code", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.GetInviteCodeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/groups/{id}/participants", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.ListParticipantsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/groups/join-info", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.GetJoinInfoAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/hybrid-link", + "status": "covered", + "sdkMethod": "MessagingClient.HybridLink.StateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OnboardingTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/identities/resolve", + "status": "covered", + "sdkMethod": "MessagingClient.Identities.ResolveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/labels", + "status": "covered", + "sdkMethod": "MessagingClient.Labels.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/labels/chats/{conversation}", + "status": "covered", + "sdkMethod": "MessagingClient.Labels.ListForChatAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/meta-pricing", + "status": "covered", + "sdkMethod": "MessagingClient.Sessions.GetMetaPricingAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/observation-policy", + "status": "covered", + "sdkMethod": "MessagingClient.ObservationPolicies.RetrieveForSessionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OnboardingTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/official-groups", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/official-groups/{group}", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/official-groups/{group}/invite-link", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.GetInviteLinkAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/official-groups/{group}/join-requests", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.ListJoinRequestsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/operations/{operationId}", + "status": "covered", + "sdkMethod": "MessagingClient.Messages.OperationStatusAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/pair/qr", + "status": "covered", + "sdkMethod": "MessagingClient.Sessions.QrAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/presence", + "status": "covered", + "sdkMethod": "MessagingClient.Presence.GetAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/presence/{conversation}", + "status": "covered", + "sdkMethod": "MessagingClient.Presence.GetForChatAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/privacy", + "status": "covered", + "sdkMethod": "MessagingClient.Privacy.GetAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/profile", + "status": "covered", + "sdkMethod": "MessagingClient.Profile.GetAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/safe-mode", + "status": "covered", + "sdkMethod": "MessagingClient.BanSafe.GetSessionSafeModeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/templates", + "status": "covered", + "sdkMethod": "MessagingClient.CloudTemplates.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/templates/{name}", + "status": "covered", + "sdkMethod": "MessagingClient.CloudTemplates.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/{session}/users/{id}/security-code", + "status": "covered", + "sdkMethod": "MessagingClient.Users.GetSecurityCodeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/bridge/route", + "status": "covered", + "sdkMethod": "BridgeClient.Routes.ResolveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SystemTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/info/status", + "status": "covered", + "sdkMethod": "SystemClient.StatusAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SystemTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/info/version", + "status": "covered", + "sdkMethod": "SystemClient.VersionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SystemTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/media/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Media.DownloadAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MediaApiTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/media/{id}/info", + "status": "covered", + "sdkMethod": "MessagingClient.Media.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MediaApiTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectId}/health-policy", + "status": "covered", + "sdkMethod": "MessagingClient.BanSafe.GetProjectHealthPolicyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectId}/insurance-evidence", + "status": "covered", + "sdkMethod": "MessagingClient.BanSafe.GetProjectInsuranceEvidenceAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectId}/observation-policy", + "status": "covered", + "sdkMethod": "MessagingClient.ObservationPolicies.RetrieveForProjectAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OnboardingTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectId}/safe-mode", + "status": "covered", + "sdkMethod": "MessagingClient.BanSafe.GetProjectSafeModeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectId}/warmup-plan", + "status": "covered", + "sdkMethod": "MessagingClient.BanSafe.GetProjectWarmupPlanAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectSlug}/campaigns", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}/analytics", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.AnalyticsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}/recipients", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.ListRecipientsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectSlug}/templates", + "status": "covered", + "sdkMethod": "MessagingClient.Templates.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/projects/{projectSlug}/templates/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Templates.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/quicklinks/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.QuickLinks.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/quicklinks/availability", + "status": "covered", + "sdkMethod": "MessagingClient.QuickLinks.AvailabilityAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/routing/hybrid", + "status": "covered", + "sdkMethod": "MessagingClient.HybridLink.GetPolicyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OnboardingTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/testing/{projectId}/events/fixtures", + "status": "covered", + "sdkMethod": "MessagingClient.Testing.ListEventFixturesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OnboardingTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/voip/calls/{id}/candidates", + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/webhooks", + "status": "covered", + "sdkMethod": "MessagingClient.Webhooks.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/messaging/webhooks/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Webhooks.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/ping", + "status": "covered", + "sdkMethod": "SystemClient.PingAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SystemTests.cs" + }, + { + "family": "messaging", + "method": "GET", + "path": "/quicklink/{token}", + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + { + "family": "messaging", + "method": "GET", + "path": "/quicklink/{token}/logo", + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + { + "family": "messaging", + "method": "PATCH", + "path": "/messaging/{session}/business/catalog/cart", + "status": "covered", + "sdkMethod": "MessagingClient.Business.SetCartEnabledAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "PATCH", + "path": "/messaging/{session}/business/collections/{collectionId}", + "status": "covered", + "sdkMethod": "MessagingClient.Business.UpdateCollectionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "PATCH", + "path": "/messaging/{session}/business/products/{productId}/visibility", + "status": "covered", + "sdkMethod": "MessagingClient.Business.SetProductVisibilityAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "PATCH", + "path": "/messaging/{session}/business/profile", + "status": "covered", + "sdkMethod": "MessagingClient.Business.UpdateProfileAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "PATCH", + "path": "/messaging/{session}/official-groups/{group}", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "PATCH", + "path": "/messaging/{session}/templates/{name}", + "status": "covered", + "sdkMethod": "MessagingClient.CloudTemplates.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "PATCH", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "PATCH", + "path": "/messaging/projects/{projectSlug}/templates/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Templates.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "PATCH", + "path": "/quicklink/{token}/testing", + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/business/catalog", + "status": "covered", + "sdkMethod": "MessagingClient.Business.CreateCatalogAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/business/collections", + "status": "covered", + "sdkMethod": "MessagingClient.Business.CreateCollectionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/business/collections/{collectionId}/appeal", + "status": "covered", + "sdkMethod": "MessagingClient.Business.AppealCollectionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/business/collections/reorder", + "status": "covered", + "sdkMethod": "MessagingClient.Business.ReorderCollectionsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/business/orders/{orderId}/lookup", + "status": "covered", + "sdkMethod": "MessagingClient.Business.GetOrderAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/business/products", + "status": "covered", + "sdkMethod": "MessagingClient.Business.CreateProductAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/business/products/{productId}/appeal", + "status": "covered", + "sdkMethod": "MessagingClient.Business.AppealProductAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/business/quick-replies", + "status": "covered", + "sdkMethod": "MessagingClient.QuickReplies.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/calls/{callId}/reject", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.RejectIncomingAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/channels", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/channels/{id}/follow", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.FollowAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/channels/{id}/live-updates", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.SubscribeToLiveUpdatesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/channels/{id}/messages/{messageId}/reaction", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.ReactToMessageAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/channels/{id}/messages/{messageId}/viewed", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.MarkMessageViewedAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/channels/{id}/mute", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.MuteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/channels/{id}/unfollow", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.UnfollowAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/channels/{id}/unmute", + "status": "covered", + "sdkMethod": "MessagingClient.Channels.UnmuteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/chats/{conversation}/archive", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.ArchiveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/chats/{conversation}/unarchive", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.UnarchiveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/cloud-credentials/reauthorize", + "status": "covered", + "sdkMethod": "MessagingClient.Sessions.ReauthorizeCloudCredentialsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/contacts/{contactId}/block", + "status": "covered", + "sdkMethod": "MessagingClient.Contacts.BlockAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/contacts/{contactId}/unblock", + "status": "covered", + "sdkMethod": "MessagingClient.Contacts.UnblockAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/groups", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/groups/{id}/admin/demote", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.DemoteParticipantsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/groups/{id}/admin/promote", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.PromoteParticipantsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/groups/{id}/invite-code/revoke", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.RevokeInviteCodeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/groups/{id}/leave", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.LeaveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/groups/{id}/participants/add", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.AddParticipantsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/groups/{id}/participants/remove", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.RemoveParticipantsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/groups/join", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.JoinAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/labels", + "status": "covered", + "sdkMethod": "MessagingClient.Labels.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/messages/react", + "status": "covered", + "sdkMethod": "MessagingClient.Messages.ReactAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/messages/seen", + "status": "covered", + "sdkMethod": "MessagingClient.Messages.MarkSeenAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/messages/send", + "status": "covered", + "sdkMethod": "MessagingClient.Messages.SendAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessageContentTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/messages/star", + "status": "covered", + "sdkMethod": "MessagingClient.Messages.StarAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/messages/typing", + "status": "covered", + "sdkMethod": "MessagingClient.Messages.SetTypingAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/official-groups", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/official-groups/{group}/invite-link/reset", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.ResetInviteLinkAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/official-groups/{group}/join-requests/approve", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.ApproveJoinRequestsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/official-groups/{group}/join-requests/reject", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.RejectJoinRequestsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/official-groups/{group}/participants/remove", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.RemoveParticipantsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/official-groups/{group}/pins", + "status": "covered", + "sdkMethod": "MessagingClient.OfficialGroups.PinAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OfficialGroupTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/pair/code", + "status": "covered", + "sdkMethod": "MessagingClient.Sessions.RequestPairingCodeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/presence", + "status": "covered", + "sdkMethod": "MessagingClient.Presence.SetAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/presence/{conversation}/subscribe", + "status": "covered", + "sdkMethod": "MessagingClient.Presence.SubscribeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/{session}/templates", + "status": "covered", + "sdkMethod": "MessagingClient.CloudTemplates.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/cloud-api/embedded-signup", + "status": "covered", + "sdkMethod": "MessagingClient.CloudOnboarding.AdvanceAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OnboardingTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/features/evaluate", + "status": "excluded", + "reason": "Host-runtime feature evaluation is not a customer messaging operation in this SDK." + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/media/{id}/download-and-save", + "status": "covered", + "sdkMethod": "MessagingClient.Media.PersistAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MediaApiTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/campaigns", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}/launch", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.LaunchAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}/pause", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.PauseAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}/recipients", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.AddRecipientsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}/requeue", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.RequeueAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}/reschedule", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.RescheduleAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}/resume", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.ResumeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/campaigns/{id}/stop", + "status": "covered", + "sdkMethod": "MessagingClient.Campaigns.StopAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/templates", + "status": "covered", + "sdkMethod": "MessagingClient.Templates.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/templates/{id}/preview", + "status": "covered", + "sdkMethod": "MessagingClient.Templates.PreviewAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/projects/{projectSlug}/templates/{id}/submit", + "status": "covered", + "sdkMethod": "MessagingClient.Templates.SubmitAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/TemplateTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/quicklinks", + "status": "covered", + "sdkMethod": "MessagingClient.QuickLinks.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/testing/{projectId}/events", + "status": "covered", + "sdkMethod": "MessagingClient.Testing.TriggerEventAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OnboardingTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/testing/{projectId}/history-fixtures", + "status": "covered", + "sdkMethod": "MessagingClient.Testing.CreateHistoryFixtureAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OnboardingTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/call-links", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.CreateCallLinkAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/call-links/preview", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.PreviewCallLinkAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.PlaceAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/accept", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.AcceptAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/candidate", + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/hand", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.SetHandRaisedAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/leave", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.LeaveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/offer", + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/participants", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.AddParticipantAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/participants/ring", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.RingParticipantAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/reaction", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.SendReactionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/reject", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.RejectAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/renegotiate", + "status": "excluded", + "reason": "used internally by the Calls SDK; not part of the public API" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/{id}/reports", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.ReportAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/voip/calls/check", + "status": "covered", + "sdkMethod": "MessagingClient.Calls.CheckAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/messaging/webhooks", + "status": "covered", + "sdkMethod": "MessagingClient.Webhooks.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "POST", + "path": "/quicklink/{token}/cancel", + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + { + "family": "messaging", + "method": "POST", + "path": "/quicklink/{token}/cloud", + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + { + "family": "messaging", + "method": "POST", + "path": "/quicklink/{token}/code", + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + { + "family": "messaging", + "method": "POST", + "path": "/quicklink/{token}/confirm", + "status": "excluded", + "reason": "Credential-free public QuickLink participant route used only by the hosted QuickLink page, not the SDKs." + }, + { + "family": "messaging", + "method": "POST", + "path": "/quicklink/{token}/connection", + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + { + "family": "messaging", + "method": "POST", + "path": "/quicklink/{token}/history", + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + { + "family": "messaging", + "method": "POST", + "path": "/quicklink/{token}/phone", + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + { + "family": "messaging", + "method": "POST", + "path": "/quicklink/{token}/phone/reset", + "status": "excluded", + "reason": "Public onboarding capability route is owned by the hosted QuickLink flow, not a machine credential resource." + }, + { + "family": "messaging", + "method": "POST", + "path": "/quicklink/{token}/testing/start", + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/business/compliance", + "status": "covered", + "sdkMethod": "MessagingClient.Business.SetMerchantComplianceAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/business/products/{productId}", + "status": "covered", + "sdkMethod": "MessagingClient.Business.UpdateProductAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/business/profile/cover-photo", + "status": "covered", + "sdkMethod": "MessagingClient.Business.SetCoverPhotoAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BusinessTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/business/quick-replies/{quickReplyId}", + "status": "covered", + "sdkMethod": "MessagingClient.QuickReplies.ReplaceAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ChannelCloudTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/chats/{conversation}/disappearing", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.SetDisappearingTimerAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/chats/{conversation}/messages/{messageId}", + "status": "covered", + "sdkMethod": "MessagingClient.Chats.EditMessageAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/groups/{id}/description", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.SetDescriptionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/groups/{id}/picture", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.SetPictureAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/groups/{id}/settings/info-edit", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.SetInfoEditingAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/groups/{id}/settings/join-approval", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.SetJoinApprovalAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/groups/{id}/settings/member-add", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.SetMemberAddModeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/groups/{id}/settings/messages", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.SetMessagingAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/groups/{id}/subject", + "status": "covered", + "sdkMethod": "MessagingClient.Groups.SetSubjectAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/hybrid-link", + "status": "covered", + "sdkMethod": "MessagingClient.HybridLink.SetPausedAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OnboardingTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/labels/{labelId}", + "status": "covered", + "sdkMethod": "MessagingClient.Labels.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/labels/chats/{conversation}", + "status": "covered", + "sdkMethod": "MessagingClient.Labels.ReplaceForChatAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/privacy/{setting}", + "status": "covered", + "sdkMethod": "MessagingClient.Privacy.SetAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/privacy/disappearing/default", + "status": "covered", + "sdkMethod": "MessagingClient.Privacy.SetDefaultDisappearingTimerAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/profile/name", + "status": "covered", + "sdkMethod": "MessagingClient.Profile.SetNameAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/profile/picture", + "status": "covered", + "sdkMethod": "MessagingClient.Profile.SetPictureAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/profile/status", + "status": "covered", + "sdkMethod": "MessagingClient.Profile.SetStatusAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ResourceTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/{session}/safe-mode", + "status": "covered", + "sdkMethod": "MessagingClient.BanSafe.UpdateSessionSafeModeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/projects/{projectId}/health-policy", + "status": "covered", + "sdkMethod": "MessagingClient.BanSafe.UpdateProjectHealthPolicyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/projects/{projectId}/insurance-evidence", + "status": "covered", + "sdkMethod": "MessagingClient.BanSafe.UpdateProjectInsuranceEvidenceAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/projects/{projectId}/safe-mode", + "status": "covered", + "sdkMethod": "MessagingClient.BanSafe.UpdateProjectSafeModeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/projects/{projectId}/warmup-plan", + "status": "covered", + "sdkMethod": "MessagingClient.BanSafe.UpdateProjectWarmupPlanAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/routing/hybrid", + "status": "covered", + "sdkMethod": "MessagingClient.HybridLink.SetPolicyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OnboardingTests.cs" + }, + { + "family": "messaging", + "method": "PUT", + "path": "/messaging/webhooks/{id}", + "status": "covered", + "sdkMethod": "MessagingClient.Webhooks.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MessagingUtilityTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/api/account/meta-app", + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/auth/factors/{factorId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/auth/providers/{provider}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/call-opt-outs/{optOutId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/calls/{callId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/members/{memberId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/organization/avatar", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/profile/avatar", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/quicklink/logo", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/sip-trunks/{trunkId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/voice/audio/{assetId}", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/voice/provider-credentials/{credentialId}", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/console/webhooks/{webhookId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/audiences/{listId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Audiences.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AudienceSettingsTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/audiences/{listId}/members/{phone}", + "status": "covered", + "sdkMethod": "OrganizationClient.Audiences.DeleteMemberAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AudienceSettingsTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/call-opt-outs/{optOutId}", + "status": "covered", + "sdkMethod": "OrganizationClient.CallOptOuts.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallConsentTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/campaigns/{campaignId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/customers/{customerId}/pairing-links/{pairingLinkId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.RevokePairingLinkAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/flows/{flowId}", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/flows/{flowId}/endpoint", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.DeleteEndpointAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/functions/{functionId}", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/functions/{functionId}/secrets/{versionId}", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.Secrets.RevokeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/keys/{keyId}", + "status": "covered", + "sdkMethod": "OrganizationClient.ApiKeys.DeactivateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/media/{mediaId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Media.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MediaApiTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/optouts/{phone}", + "status": "covered", + "sdkMethod": "OrganizationClient.OptOuts.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/UsageTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/projects/{projectId}/webhooks/{webhookId}", + "status": "covered", + "sdkMethod": "ProjectClient.Webhooks.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/sessions/{session}/client-rules", + "status": "covered", + "sdkMethod": "OrganizationClient.ClientTokens.DeleteRulesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/sessions/{sessionId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/sip-trunks/{trunkId}", + "status": "covered", + "sdkMethod": "OrganizationClient.SipTrunks.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SipTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/templates/{templateId}", + "status": "excluded", + "reason": "This dashboard-only route does not accept the team key used by Client." + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/voice/audio/{assetId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.Audio.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/voice/provider-credentials/{credentialId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.ProviderCredentials.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "DELETE", + "path": "/platform/webhooks/{webhookId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Webhooks.DeleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/api/account/cloud-health", + "status": "excluded", + "reason": "Console-only redacted account health requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/api/account/cloud-health/export", + "status": "excluded", + "reason": "Console-only redacted account health export requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/api/account/meta-app", + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/audit", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; team keys and project tokens are not accepted." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/auth/methods", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/auth/providers", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/call-opt-outs", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/call-policy", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/call-retention", + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/call-settings/{sessionId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/calls", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/calls/{callId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/deployments", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/insights/analytics", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/insights/log-calendar", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; team keys and project tokens are not accepted." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/insights/logs", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/insights/number-options", + "status": "excluded", + "reason": "Private Console selector requires dashboard identity; team keys and project tokens are not accepted." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/insights/preferences", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/insights/usage", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/notifications", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/notifications/archived", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/notifications/count", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/notifications/preferences", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/organizations/{slug}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/profile", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/projects/{projectId}/logging", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; team keys and project tokens are not accepted." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/projects/{projectId}/setup", + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/sandbox/pool", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/session-configuration/{sessionId}", + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/sip-trunks", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/sip-trunks/{trunkId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/sip/endpoint", + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/teams", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/voice/audio", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/voice/audio/{assetId}", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/voice/audio/{assetId}/preview", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/voice/provider-credentials", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/voice/provider-credentials/{credentialId}", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "GET", + "path": "/console/webhooks", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/audiences", + "status": "covered", + "sdkMethod": "OrganizationClient.Audiences.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AudienceSettingsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/audiences/{listId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Audiences.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AudienceSettingsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/audiences/{listId}/members", + "status": "covered", + "sdkMethod": "OrganizationClient.Audiences.ListMembersAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AudienceSettingsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/audit", + "status": "covered", + "sdkMethod": "OrganizationClient.AuditLogs.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bans", + "status": "covered", + "sdkMethod": "OrganizationClient.SessionBans.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bans/active", + "status": "covered", + "sdkMethod": "OrganizationClient.SessionBans.ListActiveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/claims", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.ListClaimsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/claims/{claimId}", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.GetClaimAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/collection", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.ListCollectionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/enforcement", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.ListEnforcementAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/findings", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.ListFindingsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/health", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.ListHealthAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/health-actions", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.ListHealthActionsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/health/{session}", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.GetHealthAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/health/{session}/history", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.ListHealthHistoryAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/incidents", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.ListIncidentsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/signals", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.ListSignalsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/telemetry/{session}", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.GetTelemetryAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/bansafe/telemetry/{session}/history", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.ListTelemetryHistoryAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/billing", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/billing/controls/{scope}/{resourceId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.GetResourceControlsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/billing/limits", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.GetLimitsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/billing/pricing", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.ListPricingAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/billing/priorities", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.GetPrioritiesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/billing/transactions", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.ListTransactionsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/billing/usage", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.UsageAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/call-opt-outs", + "status": "covered", + "sdkMethod": "OrganizationClient.CallOptOuts.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallConsentTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/call-policy", + "status": "covered", + "sdkMethod": "OrganizationClient.CallPolicy.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallConsentTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/call-retention", + "status": "covered", + "sdkMethod": "OrganizationClient.CallRetention.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallConsentTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/calls", + "status": "covered", + "sdkMethod": "OrganizationClient.Calls.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallAnalyticsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/calls/{callId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Calls.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallAnalyticsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/calls/export", + "status": "covered", + "sdkMethod": "OrganizationClient.Calls.ExportAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallAnalyticsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/calls/stats", + "status": "covered", + "sdkMethod": "OrganizationClient.Calls.StatsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallAnalyticsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/campaigns", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/campaigns/{campaignId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/campaigns/{campaignId}/analytics", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.AnalyticsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/campaigns/{campaignId}/conversions", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.ConversionsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/campaigns/{campaignId}/events", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.EventsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/campaigns/{campaignId}/recipients", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.RecipientsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/customers", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/customers/{customerId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/customers/{customerId}/events", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.ListEventsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/customers/{customerId}/numbers", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.ListNumbersAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/customers/{customerId}/pairing-links", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.ListPairingLinksAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/events", + "status": "covered", + "sdkMethod": "OrganizationClient.Events.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/EventsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/events/{eventId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Events.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/EventsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/flow-encryption-keys", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.EncryptionKeyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/flows", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/flows/{flowId}", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/flows/{flowId}/endpoint", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.EndpointAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/flows/{flowId}/endpoint/receipts", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.EndpointReceiptsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/flows/{flowId}/receipts", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.ReceiptsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/functions", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/functions/{functionId}", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/functions/{functionId}/deployments", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.Deployments.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/functions/{functionId}/deployments/{deploymentId}", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.Deployments.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/functions/{functionId}/invocations", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.Invocations.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/functions/{functionId}/invocations/{invocationId}", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.Invocations.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/functions/{functionId}/secrets", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.Secrets.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/gates", + "status": "covered", + "sdkMethod": "OrganizationClient.Usage.ListGatesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/UsageTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/incidents", + "status": "covered", + "sdkMethod": "OrganizationClient.SecurityIncidents.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/keys", + "status": "covered", + "sdkMethod": "OrganizationClient.ApiKeys.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/media/{mediaId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Media.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MediaApiTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/members", + "status": "covered", + "sdkMethod": "OrganizationClient.Members.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/operations", + "status": "covered", + "sdkMethod": "OrganizationClient.Operations.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OperationsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/operations/{operationId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Operations.GetAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OperationsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/operations/{operationId}/transitions", + "status": "covered", + "sdkMethod": "OrganizationClient.Operations.ListTransitionsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OperationsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/optouts", + "status": "covered", + "sdkMethod": "OrganizationClient.OptOuts.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/UsageTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/optouts/settings", + "status": "covered", + "sdkMethod": "OrganizationClient.OptOuts.GetSettingsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/UsageTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/customers/status", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.StatusAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/events", + "status": "covered", + "sdkMethod": "ProjectClient.Events.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/EventsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/events/{eventId}", + "status": "covered", + "sdkMethod": "ProjectClient.Events.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/EventsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/events/stream", + "status": "covered", + "sdkMethod": "ProjectClient.Events.Stream", + "testFile": "tests/Polymorfa.Sdk.Tests/EventsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/health-policy", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.GetHealthPolicyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/hybrid-merge-candidates", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.ListHybridMergeCandidatesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/insurance-evidence", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.GetInsuranceEvidenceAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/operations", + "status": "covered", + "sdkMethod": "ProjectClient.Operations.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OperationsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/operations/{operationId}", + "status": "covered", + "sdkMethod": "ProjectClient.Operations.GetAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OperationsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/operations/{operationId}/transitions", + "status": "covered", + "sdkMethod": "ProjectClient.Operations.ListTransitionsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OperationsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/safe-mode", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.GetSafeModeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/warmup-plan", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.GetWarmupPlanAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/webhook-deliveries", + "status": "covered", + "sdkMethod": "ProjectClient.WebhookDeliveries.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/webhook-deliveries/{deliveryId}", + "status": "covered", + "sdkMethod": "ProjectClient.WebhookDeliveries.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/webhook-deliveries/{deliveryId}/attempts", + "status": "covered", + "sdkMethod": "ProjectClient.WebhookDeliveries.ListAttemptsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/webhook-deliveries/{deliveryId}/attempts/{attemptId}", + "status": "covered", + "sdkMethod": "ProjectClient.WebhookDeliveries.RetrieveAttemptAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/webhooks", + "status": "covered", + "sdkMethod": "ProjectClient.Webhooks.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/webhooks/{webhookId}", + "status": "covered", + "sdkMethod": "ProjectClient.Webhooks.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/quicklink", + "status": "covered", + "sdkMethod": "ProjectClient.QuickLinkSettings.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AudienceSettingsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/session-configuration", + "status": "covered", + "sdkMethod": "OrganizationClient.Configuration.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sessions", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sessions/{session}/call-settings", + "status": "covered", + "sdkMethod": "OrganizationClient.Calls.RetrieveCallSettingsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sessions/{session}/client-rules", + "status": "covered", + "sdkMethod": "OrganizationClient.ClientTokens.RetrieveRulesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sessions/{sessionId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sessions/{sessionId}/capabilities", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.GetCapabilitiesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sessions/{sessionId}/me", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.AccountAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sessions/{sessionId}/safe-mode", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.GetSafeModeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sessions/{sessionId}/tier-quotes/{quoteId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.RetrieveTierChangeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sip-trunks", + "status": "covered", + "sdkMethod": "OrganizationClient.SipTrunks.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SipTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sip-trunks/{trunkId}", + "status": "covered", + "sdkMethod": "OrganizationClient.SipTrunks.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SipTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/sip/endpoint", + "status": "covered", + "sdkMethod": "OrganizationClient.SipTrunks.EndpointAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SipTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/team", + "status": "covered", + "sdkMethod": "OrganizationClient.Organizations.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/templates", + "status": "excluded", + "reason": "This dashboard-only route does not accept the team key used by Client." + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/templates/{templateId}", + "status": "excluded", + "reason": "This dashboard-only route does not accept the team key used by Client." + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/tokens", + "status": "covered", + "sdkMethod": "OrganizationClient.ProjectTokens.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/usage", + "status": "covered", + "sdkMethod": "OrganizationClient.Usage.SummaryAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/UsageTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/usage/records", + "status": "covered", + "sdkMethod": "OrganizationClient.Usage.ListRecordsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/UsageTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/voice/audio", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.Audio.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/voice/audio/{assetId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.Audio.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/voice/audio/{assetId}/preview", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.Audio.PreviewUrlAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/voice/provider-credentials", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.ProviderCredentials.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/voice/provider-credentials/{credentialId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.ProviderCredentials.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/webhook-deliveries", + "status": "covered", + "sdkMethod": "OrganizationClient.WebhookDeliveries.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/webhook-deliveries/{deliveryId}", + "status": "covered", + "sdkMethod": "OrganizationClient.WebhookDeliveries.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/webhook-deliveries/{deliveryId}/attempts", + "status": "covered", + "sdkMethod": "OrganizationClient.WebhookDeliveries.ListAttemptsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/webhook-deliveries/{deliveryId}/attempts/{attemptId}", + "status": "covered", + "sdkMethod": "OrganizationClient.WebhookDeliveries.RetrieveAttemptAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/webhooks", + "status": "covered", + "sdkMethod": "OrganizationClient.Webhooks.ListAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/webhooks/{webhookId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Webhooks.RetrieveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "PATCH", + "path": "/console/members/{memberId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PATCH", + "path": "/console/organization", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PATCH", + "path": "/console/profile", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PATCH", + "path": "/console/projects/{projectId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PATCH", + "path": "/console/sip-trunks/{trunkId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PATCH", + "path": "/console/voice/audio/{assetId}", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "PATCH", + "path": "/console/webhooks/{webhookId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PATCH", + "path": "/platform/campaigns/{campaignId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "PATCH", + "path": "/platform/customers/{customerId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "PATCH", + "path": "/platform/flows/{flowId}", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "PATCH", + "path": "/platform/functions/{functionId}", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "PATCH", + "path": "/platform/projects/{projectId}/webhooks/{webhookId}", + "status": "covered", + "sdkMethod": "ProjectClient.Webhooks.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "PATCH", + "path": "/platform/sessions/{sessionId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.SetTierOverrideAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "PATCH", + "path": "/platform/sip-trunks/{trunkId}", + "status": "covered", + "sdkMethod": "OrganizationClient.SipTrunks.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SipTests.cs" + }, + { + "family": "platform", + "method": "PATCH", + "path": "/platform/templates/{templateId}", + "status": "excluded", + "reason": "This dashboard-only route does not accept the team key used by Client." + }, + { + "family": "platform", + "method": "PATCH", + "path": "/platform/voice/audio/{assetId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.Audio.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "PATCH", + "path": "/platform/webhooks/{webhookId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Webhooks.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/api/account/features/enrollment", + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + { + "family": "platform", + "method": "POST", + "path": "/api/account/features/evaluate", + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + { + "family": "platform", + "method": "POST", + "path": "/api/account/features/exposure", + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/auth/totp", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/auth/totp/verify", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/bansafe/enforcement/{session}/appeal", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/bansafe/findings/{findingId}/acknowledge", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/billing/topup", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/call-opt-outs", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/call-opt-outs/import", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/calls/{callId}/connections/{connectionId}/remove", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/keys", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/members/invites", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/notifications/{notificationId}/archive", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/notifications/{notificationId}/read", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/notifications/archive", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/notifications/read", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/notifications/test", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/organization-invitations/{invitationId}/accept", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/organization/avatar/uploads", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/organizations/ensure", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/profile/avatar/uploads", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/profile/ensure", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/projects/{projectId}/icon", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/projects/{projectId}/setup/usage", + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/quicklink/logo/org", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/quicklink/logo/project", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/quicklink/logo/uploads", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/sip-trunks", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/sip-trunks/{trunkId}/credentials", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/teams/{teamId}/delete", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/teams/leave", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/voice/audio", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/voice/audio/{assetId}/complete", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/voice/audio/tts", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/voice/provider-credentials", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/voice/provider-credentials/{credentialId}/verify", + "status": "excluded", + "reason": "Console-only voice routes require dashboard membership; server SDK uses Platform voice routes." + }, + { + "family": "platform", + "method": "POST", + "path": "/console/webhooks", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/audiences", + "status": "covered", + "sdkMethod": "OrganizationClient.Audiences.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AudienceSettingsTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/audiences/{listId}/members", + "status": "covered", + "sdkMethod": "OrganizationClient.Audiences.AddMembersAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AudienceSettingsTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/audiences/uploads", + "status": "covered", + "sdkMethod": "OrganizationClient.Audiences.CreateUploadAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AudienceSettingsTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/bansafe/incidents", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.CreateIncidentAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/bansafe/incidents/{incidentId}/retract", + "status": "covered", + "sdkMethod": "OrganizationClient.BanSafe.RetractIncidentAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BanSafeTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/call-opt-outs", + "status": "covered", + "sdkMethod": "OrganizationClient.CallOptOuts.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallConsentTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/call-opt-outs/import", + "status": "covered", + "sdkMethod": "OrganizationClient.CallOptOuts.ImportAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallConsentTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/archive", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.ArchiveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/conversions", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.RecordConversionAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/duplicate", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.DuplicateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/launch", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.LaunchAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/pause", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.PauseAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/recipients", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.AddRecipientsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/requeue", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.RequeueAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/reschedule", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.RescheduleAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/resume", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.ResumeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/campaigns/{campaignId}/stop", + "status": "covered", + "sdkMethod": "OrganizationClient.Campaigns.StopAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CampaignTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/client-tokens", + "status": "covered", + "sdkMethod": "OrganizationClient.ClientTokens.MintAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/customers", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/customers/{customerId}/archive", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.ArchiveAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/customers/{customerId}/numbers/{sessionId}/transfer", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.TransferNumberAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/customers/{customerId}/pairing-links", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.CreatePairingLinkAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/customers/{customerId}/restore", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.RestoreAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/events/{eventId}/replays", + "status": "covered", + "sdkMethod": "OrganizationClient.Events.ReplayAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/EventsTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/flow-encryption-keys/rotate", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.RotateEncryptionKeyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/flows", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/flows/{flowId}/deprecate", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.DeprecateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/flows/{flowId}/discard", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.DiscardAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/flows/{flowId}/publish", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.PublishAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/flows/{flowId}/sync", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.SyncAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/flows/{flowId}/upload", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.UploadAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/functions", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/functions/{functionId}/deployments", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.Deployments.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/functions/{functionId}/invocations", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.Invocations.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/functions/{functionId}/secrets", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.Secrets.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/incidents/{incidentId}/acknowledge", + "status": "covered", + "sdkMethod": "OrganizationClient.SecurityIncidents.AcknowledgeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/media/uploads", + "status": "covered", + "sdkMethod": "OrganizationClient.Media.CreateUploadAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/MediaApiTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/operations/{operationId}/cancel", + "status": "covered", + "sdkMethod": "OrganizationClient.Operations.CancelAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OperationsTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/optouts", + "status": "covered", + "sdkMethod": "OrganizationClient.OptOuts.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/UsageTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/optouts/batch", + "status": "covered", + "sdkMethod": "OrganizationClient.OptOuts.CreateBatchAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/UsageTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/customers/enable", + "status": "covered", + "sdkMethod": "OrganizationClient.Customers.EnableAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CustomerTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/events/{eventId}/replays", + "status": "covered", + "sdkMethod": "ProjectClient.Events.ReplayAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/EventsTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/events/stream/{streamId}/ack", + "status": "covered", + "sdkMethod": "OrganizationClient.Events.AcknowledgeStreamAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/EventsTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/operations/{operationId}/cancel", + "status": "covered", + "sdkMethod": "ProjectClient.Operations.CancelAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/OperationsTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/production-enrollments/{operationId}/approve", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.ApproveProductionEnrollmentAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/production-enrollments/{operationId}/cancel", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.CancelProductionEnrollmentAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/promote", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.RequestProductionEnrollmentAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/webhook-deliveries/{deliveryId}/retry", + "status": "covered", + "sdkMethod": "ProjectClient.WebhookDeliveries.RetryAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/webhooks", + "status": "covered", + "sdkMethod": "ProjectClient.Webhooks.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/webhooks/{webhookId}/secret-rotations", + "status": "covered", + "sdkMethod": "ProjectClient.Webhooks.RotateSecretAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/projects/{projectId}/webhooks/{webhookId}/tests", + "status": "covered", + "sdkMethod": "ProjectClient.Webhooks.TestAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/handoff-ack", + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/handoff-import", + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/handoff-keys", + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/logout", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.LogoutAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/restart", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.RestartAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectPairingTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/start", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.StartAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/stop", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.StopAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/tier-quotes", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.QuoteTierChangeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/{sessionId}/upload-appstate-keys", + "status": "excluded", + "reason": "Encrypted session handoff remains outside the public SDK until its dedicated security and atomicity contract is complete." + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/delete", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.DeleteManyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sessions/stop", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.StopManyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sip-trunks", + "status": "covered", + "sdkMethod": "OrganizationClient.SipTrunks.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SipTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/sip-trunks/{trunkId}/credentials", + "status": "covered", + "sdkMethod": "OrganizationClient.SipTrunks.RotateCredentialsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SipTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/templates", + "status": "excluded", + "reason": "This dashboard-only route does not accept the team key used by Client." + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/voice/audio", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.Audio.CreateUploadAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/voice/audio/{assetId}/complete", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.Audio.CompleteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/voice/audio/tts", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.Audio.SynthesizeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/voice/provider-credentials", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.ProviderCredentials.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/voice/provider-credentials/{credentialId}/verify", + "status": "covered", + "sdkMethod": "OrganizationClient.Voice.ProviderCredentials.VerifyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/VoiceTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/webhook-deliveries/{deliveryId}/retry", + "status": "covered", + "sdkMethod": "OrganizationClient.WebhookDeliveries.RetryAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/webhooks", + "status": "covered", + "sdkMethod": "OrganizationClient.Webhooks.CreateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/webhooks/{webhookId}/secret-rotations", + "status": "covered", + "sdkMethod": "OrganizationClient.Webhooks.RotateSecretAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "POST", + "path": "/platform/webhooks/{webhookId}/tests", + "status": "covered", + "sdkMethod": "OrganizationClient.Webhooks.TestAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/PlatformWebhookTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/api/account/meta-app", + "status": "excluded", + "reason": "Dashboard identity and account controls require a live browser account session; not part of the machine SDK." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/call-policy", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/call-retention", + "status": "excluded", + "reason": "Console-only dashboard operation; not a public SDK method." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/call-settings/{sessionId}", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/insights/preferences", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/notifications/preferences", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/organization/avatar", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/profile/avatar", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/projects/{projectId}/logging/drains", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; team keys and project tokens are not accepted." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/projects/{projectId}/logging/sampling", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; team keys and project tokens are not accepted." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/projects/{projectId}/setup", + "status": "excluded", + "reason": "Console-only project setup uses dashboard identity and is unavailable to SDK credentials." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/quicklink/logo", + "status": "excluded", + "reason": "Console-only operation requires dashboard identity; not available to SDK credentials." + }, + { + "family": "platform", + "method": "PUT", + "path": "/console/session-configuration/{sessionId}", + "status": "excluded", + "reason": "Hosted QuickLink recipient and authenticated Console-only operations are outside the trusted-server SDK surface." + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/billing/controls/{scope}/{resourceId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.SetResourceControlsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/billing/limits/{scope}/{resourceId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.SetLimitAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/billing/priorities", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.ReorderPrioritiesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/billing/priorities/{scope}/{resourceId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Billing.SetPriorityAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/BillingTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/call-policy", + "status": "covered", + "sdkMethod": "OrganizationClient.CallPolicy.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallConsentTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/call-retention", + "status": "covered", + "sdkMethod": "OrganizationClient.CallRetention.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallConsentTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/flows/{flowId}/endpoint", + "status": "covered", + "sdkMethod": "ProjectClient.Flows.SetEndpointAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FlowTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/functions/{functionId}/promotion", + "status": "covered", + "sdkMethod": "ProjectClient.Functions.Deployments.PromoteAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/FunctionTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/optouts/settings", + "status": "covered", + "sdkMethod": "OrganizationClient.OptOuts.UpdateSettingsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/UsageTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/projects/{projectId}/health-policy", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.UpdateHealthPolicyAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/projects/{projectId}/insurance-evidence", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.UpdateInsuranceEvidenceAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/projects/{projectId}/safe-mode", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.UpdateSafeModeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/projects/{projectId}/warmup-plan", + "status": "covered", + "sdkMethod": "OrganizationClient.Projects.UpdateWarmupPlanAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/ProjectSettingsTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/quicklink", + "status": "covered", + "sdkMethod": "ProjectClient.QuickLinkSettings.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AudienceSettingsTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/session-configuration", + "status": "covered", + "sdkMethod": "OrganizationClient.Configuration.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/sessions/{session}/call-settings", + "status": "covered", + "sdkMethod": "OrganizationClient.Calls.UpdateCallSettingsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallsTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/sessions/{session}/client-rules", + "status": "covered", + "sdkMethod": "OrganizationClient.ClientTokens.UpdateRulesAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/AccountTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/sessions/{sessionId}", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.UpdateAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "PUT", + "path": "/platform/sessions/{sessionId}/safe-mode", + "status": "covered", + "sdkMethod": "OrganizationClient.Sessions.UpdateSafeModeAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/SessionTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/analytics", + "status": "covered", + "sdkMethod": "OrganizationClient.Analytics.GetAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallAnalyticsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/analytics", + "status": "covered", + "sdkMethod": "ProjectClient.Analytics.GetAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallAnalyticsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/analytics/metrics", + "status": "covered", + "sdkMethod": "OrganizationClient.Analytics.MetricsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallAnalyticsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/projects/{projectId}/analytics/metrics", + "status": "covered", + "sdkMethod": "ProjectClient.Analytics.MetricsAsync", + "testFile": "tests/Polymorfa.Sdk.Tests/CallAnalyticsTests.cs" + }, + { + "family": "platform", + "method": "GET", + "path": "/console/insights/vitals", + "status": "excluded", + "reason": "Dashboard-only operational error vitals require a live Console identity; server API keys do not grant this authority." + } + ], + "supplements": { + "officialApiRound2": { + "inventory": "contracts/native-official-round2.json", + "language": "dotnet", + "claim": "Source and native wire-fixture attribution, separate from baseline operations and runtime qualification." + }, + "simultaneousCalls": { + "inventory": "contracts/native-calls-simultaneous-calls.json", + "baselineUnchanged": true, + "published": false, + "language": "dotnet" + } + } +} diff --git a/packages/dotnet/examples/README.md b/packages/dotnet/examples/README.md new file mode 100644 index 00000000..ae287355 --- /dev/null +++ b/packages/dotnet/examples/README.md @@ -0,0 +1,54 @@ +# .NET integrations + +These source examples use the development SDK. They do not establish registry or hosted API availability. + +## ASP.NET Core + +`aspnet-core` compiles against .NET 8. Set `POLYMORFA_PROJECT_TOKEN` (or `POLYMORFA_API_KEY`), `POLYMORFA_SESSION`, `POLYMORFA_RECIPIENT` and `POLYMORFA_WEBHOOK_SECRET` in server configuration. Run `dotnet run --project aspnet-core/Polymorfa.Example.AspNet.csproj` from this directory. + +The Development-only sign-in route requires `POLYMORFA_EXAMPLE_PASSWORD`; it is disabled without that setting or outside Development. It signs an opaque HttpOnly cookie, validates its server-owned session on every request and revokes it at logout. Restarting this example clears its sessions and signing authority. Replace this demonstration identity with your application's authentication scheme before deployment. Token requests require an authenticated subject and a `polymorfa_session` claim equal to the configured session. Resolve that claim from trusted application authorization; never copy a browser-supplied session or customer identifier into it. The token endpoint uses POST, checks the same origin, returns a five-minute client token and disables caching. The token body includes the browser grant; untrusted request fields cannot change the subject or session. + +The webhook endpoint verifies the exact bounded raw body using `x-webhook-signature`. The ASP.NET native inbox projects assigned-Number replies and receipts into a bounded application-owned conversation after signature verification. Do not log the request body. + +From the repository root run `npm ci`, `npm run build:workspaces`, and `node packages/dotnet/examples/build-elements.mjs` before starting the example. It mounts the existing Web Components with a custom application data source. Every +text send calls the native C# SDK on the server. Replies arrive through a signed +native webhook, restricted to the assigned Number and canonical conversation ID. +The state holds up to 100 messages, 128 send attempts and 1024 recent event IDs +until the server stops. It does not enable HMS. A UUID key belongs to one text; +unknown outcomes retain it for retry. Display timestamps use whole seconds, +matching native events. Reload restores the authenticated inbox. + +Set `POLYMORFA_EXAMPLE_TRANSPORT=local-test-number` to label simulated transport. +The local Inspector recording covers the ASP.NET custom UI, a paired simulated +WhatsApp peer, signed HTTPS delivery, reload and logout. It does not establish +registry, staging or production availability. Blazor and MAUI below still use +their separate browser-grant renderer and empty history route; this recording +does not establish their send/receive acceptance. Server credentials stay in +ASP.NET Core. + +## Blazor WebAssembly + +`blazor-components` provides the same reusable Razor component for either host. It compiles independently; it does not reference the server SDK or contain server credentials. Reference its project from the host, bundle `wwwroot/messaging.ts` to `messaging.js`, and render ``. The component imports the module after rendering and disposes the browser client when removed. + +`blazor-wasm` is a runnable same-origin WebAssembly host. Run `dotnet publish packages/dotnet/examples/blazor-wasm/Polymorfa.Example.Blazor.csproj --configuration Release --output packages/dotnet/examples/blazor-wasm/bin/served` and `node packages/dotnet/examples/prepare-blazor.mjs` from the repository root. Start ASP.NET Core, sign in at `/`, then open `/blazor/`. It uses the authenticated ASP.NET Core token endpoint and server-owned API configuration above. The WebAssembly host is checked in a browser against local API fixtures. + +Use one mounted component per browser client. Install and bundle the same accepted TypeScript package version as the owning release. These integrations reuse `@polymorfa/elements`; they do not implement another messaging or calling UI. + +## MAUI Blazor Hybrid + +`maui` is a runnable Mac Catalyst host for the same Razor component. Its native application bridge owns the authenticated application cookie and allowlists fixed config, token, history and connection routes. JavaScript receives short-lived client tokens; it never receives the application cookie or a server API key. The application origin is a build-owned setting. Production configuration requires HTTPS; loopback HTTP is limited to the debug example. + +See [the MAUI instructions](maui/README.md) for the supported workload, build and native bridge tests. Local Mac Catalyst runtime proof covers sign-in, component mounting, token minting, inbox loading and logout against synthetic API fixtures. Windows, Android and iOS hosts are not supplied or exercised. + +## Programmatic Calls + +`calls-agent` connects the server lifecycle socket, answers incoming calls and consumes typed PCM frames. Set `POLYMORFA_API_KEY` and `POLYMORFA_SESSION`, then run `dotnet run --project calls-agent/Polymorfa.Example.Calls.csproj`. Add consent and application policy before answering. Ctrl+C cancels the event and media consumers. Production providers can use `CallsTokenSource` to supply and refresh short-lived credentials. + +The client reconnects media with the same connection identity, restores acknowledged media preferences and bounds retry attempts. Browser call media remains in the TypeScript Calls package. + +The authenticated ASP.NET connect route adds Linked Devices to its server-assigned +Number. Set `POLYMORFA_SESSION` to an existing Number with an eligible Official +API connection. The route requests a supplementary hybrid QuickLink and ignores +client-supplied session choices. Initial invitations must omit session and +addConnection. Blazor and MAUI use this same authenticated route; bridge/build +checks do not establish browser pairing or live provider acceptance. diff --git a/packages/dotnet/examples/aspnet-core/NativeConversation.cs b/packages/dotnet/examples/aspnet-core/NativeConversation.cs new file mode 100644 index 00000000..a853df01 --- /dev/null +++ b/packages/dotnet/examples/aspnet-core/NativeConversation.cs @@ -0,0 +1,86 @@ +using System.Text; +using System.Text.Json; +using Polymorfa.Sdk; + +internal sealed record ExampleMessage(string Id, string? ClientId, string Text, long CreatedAt, string Direction, string Status, string Receipt); +internal sealed class NativeConversation(string session, string peer, bool localTest) +{ + private readonly object state = new(); + private readonly SemaphoreSlim sendGate = new(1, 1); + private readonly List messages = []; + private readonly Dictionary attempts = []; + private readonly Queue eventIds = new(); + private string peerId = "", lastEvent = ""; + private int received; + public object Snapshot() { lock (state) return new { messages = messages.ToArray(), peer, session, receivedEvents = received, lastEvent, localTest }; } + public async Task SendAsync(MessagingClient client, byte[] body, string key, CancellationToken cancellation) + { + string text; + try + { + using var json = JsonDocument.Parse(body); + var root = json.RootElement; + if (root.ValueKind != JsonValueKind.Object || root.EnumerateObject().Count() != 1 || !root.TryGetProperty("text", out var value) || value.ValueKind != JsonValueKind.String) return Results.BadRequest(); + text = value.GetString()!; + } + catch (JsonException) { return Results.BadRequest(); } + if (string.IsNullOrWhiteSpace(text) || Encoding.UTF8.GetByteCount(text) > 4096 || !Guid.TryParseExact(key, "D", out _)) return Results.BadRequest(); + if (!await sendGate.WaitAsync(0, cancellation)) return Results.StatusCode(429); + try + { + if (attempts.TryGetValue(key, out var prior)) + { + if (prior.Text != text) return Results.Conflict(); + if (prior.Message is not null) return Results.Json(prior.Message); + } + else + { + if (attempts.Count >= 128) return Results.StatusCode(429); + attempts[key] = (text, null); + } + try + { + var response = await client.Messages.SendAsync(session, new(new(PhoneNumber: peer), new TextContent(text)), new() { IdempotencyKey = key, Timeout = TimeSpan.FromSeconds(60), CancellationToken = cancellation }); + var receipt = response.Data.Data; + if (!response.Data.Success || string.IsNullOrEmpty(receipt.Id) || !DateTimeOffset.TryParse(receipt.Timestamp, out var at)) return Results.StatusCode(502); + var message = new ExampleMessage(receipt.Id, key, text, at.ToUnixTimeSeconds() * 1000, "outbound", "sent", receipt.Status); + lock (state) { if (!string.IsNullOrEmpty(receipt.Conversation.Id)) peerId = receipt.Conversation.Id; Upsert(message); } + attempts[key] = (text, message); + return Results.Json(message); + } + catch (Exception error) when (error is PolymorfaException or OperationCanceledException) { return Results.Json(new { error = "send_outcome_unknown_retry_same_key" }, statusCode: 502); } + } + finally { sendGate.Release(); } + } + private void Upsert(ExampleMessage message) + { + var index = messages.FindIndex(m => m.Id == message.Id); + if (index >= 0) messages[index] = message; else messages.Add(message); + if (messages.Count > 100) messages.RemoveAt(0); + } + private bool Assigned(EventConversation conversation) => peerId.Length > 0 ? conversation.Id == peerId : conversation.PhoneNumber?.TrimStart('+') == peer.TrimStart('+'); + private static int Rank(string value) => value switch { "read" or "played" => 3, "delivered" or "delivery" => 2, "sent" or "server" => 1, _ => 0 }; + public void Receive(WebhookEvent webhook) + { + var envelope = webhook.Envelope; + if (envelope.Session != session) return; + lock (state) + { + if (eventIds.Contains(envelope.Id)) return; + switch (webhook) + { + case TypedWebhookEvent { Payload: var p } when envelope.Event == "message.received": + if (p.FromMe || p.IsGroup || p.Conversation is null || !Assigned(p.Conversation) || string.IsNullOrEmpty(p.Id) || string.IsNullOrEmpty(p.Text) || Encoding.UTF8.GetByteCount(p.Text) > 4096) return; + Upsert(new(p.Id, null, p.Text, checked(p.Timestamp * 1000), "inbound", "sent", "received")); + break; + case TypedWebhookEvent { Payload: var p }: + if (p.Conversation is null || !Assigned(p.Conversation)) return; + for (var i = 0; i < messages.Count; i++) + if (messages[i].Direction == "outbound" && p.Messages.Any(m => m.Id == messages[i].Id) && Rank(p.Type) > Rank(messages[i].Receipt)) messages[i] = messages[i] with { Receipt = p.Type }; + break; + default: return; + } + received++; lastEvent = envelope.Event; eventIds.Enqueue(envelope.Id); if (eventIds.Count > 1024) eventIds.Dequeue(); + } + } +} diff --git a/packages/dotnet/examples/aspnet-core/Polymorfa.Example.AspNet.csproj b/packages/dotnet/examples/aspnet-core/Polymorfa.Example.AspNet.csproj new file mode 100644 index 00000000..52064104 --- /dev/null +++ b/packages/dotnet/examples/aspnet-core/Polymorfa.Example.AspNet.csproj @@ -0,0 +1,4 @@ + + net8.0enableenabletrue + + diff --git a/packages/dotnet/examples/aspnet-core/Program.cs b/packages/dotnet/examples/aspnet-core/Program.cs new file mode 100644 index 00000000..6d384c48 --- /dev/null +++ b/packages/dotnet/examples/aspnet-core/Program.cs @@ -0,0 +1,102 @@ +using System.Collections.Concurrent; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.DataProtection; +using Polymorfa.Sdk; + +var applicationSessions = new ConcurrentDictionary(); +var builder = WebApplication.CreateBuilder(args); +var session = builder.Configuration["POLYMORFA_SESSION"] ?? throw new InvalidOperationException("POLYMORFA_SESSION is required."); +var peer = builder.Configuration["POLYMORFA_RECIPIENT"] ?? throw new InvalidOperationException("POLYMORFA_RECIPIENT is required."); +if (string.IsNullOrWhiteSpace(peer)) throw new InvalidOperationException("An assigned recipient is required."); +var conversation = new NativeConversation(session, peer, builder.Configuration["POLYMORFA_EXAMPLE_TRANSPORT"] == "local-test-number"); +builder.Services.AddSingleton(new MessagingClient(builder.Configuration["POLYMORFA_PROJECT_TOKEN"] is { Length: > 0 } projectToken ? Credential.ProjectToken(projectToken) : Credential.OrganizationApiKey(builder.Configuration["POLYMORFA_API_KEY"] ?? throw new InvalidOperationException("Configure a server project token or organization API key.")), new() { BaseUrl = new(builder.Configuration["POLYMORFA_API_BASE_URL"] ?? "https://api.polymorfa.com") })); +// Replace this application's cookie sign-in with your own identity provider. +builder.Services.AddDataProtection().UseEphemeralDataProtectionProvider(); +builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => +{ + options.Cookie.Name = "polymorfa-example"; options.Cookie.HttpOnly = true; options.Cookie.SameSite = SameSiteMode.Strict; + options.Events.OnValidatePrincipal = context => + { + var id = context.Principal?.FindFirstValue("example_session"); + if (id is null || !applicationSessions.TryGetValue(id, out var expires) || expires <= DateTimeOffset.UtcNow) context.RejectPrincipal(); + return Task.CompletedTask; + }; + options.Events.OnRedirectToLogin = context => { context.Response.StatusCode = 401; return Task.CompletedTask; }; + options.Events.OnRedirectToAccessDenied = context => { context.Response.StatusCode = 403; return Task.CompletedTask; }; +}); +builder.Services.AddAuthorization(options => options.AddPolicy("PolymorfaSession", policy => policy.RequireAuthenticatedUser().RequireClaim("polymorfa_session", session))); +var app = builder.Build(); +var staticTypes = new Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider(); +staticTypes.Mappings[".dat"] = "application/octet-stream"; // Blazor globalization data. +app.UseDefaultFiles(); app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = staticTypes }); app.UseAuthentication(); app.UseAuthorization(); +// A local example sign-in only. No route exists outside Development or without a configured password. +if (app.Environment.IsDevelopment() && app.Configuration["POLYMORFA_EXAMPLE_PASSWORD"] is { Length: > 0 } password) +{ + app.MapPost("/example/login", async (HttpContext context) => + { + if (context.Request.Headers.Origin.ToString() != $"{context.Request.Scheme}://{context.Request.Host}" || context.Request.ContentLength > 4096) return Results.StatusCode(403); + var maximum = context.Features.Get(); if (maximum is { IsReadOnly: false }) maximum.MaxRequestBodySize = 4096; + if (!context.Request.HasFormContentType) return Results.StatusCode(415); + var form = await context.Request.ReadFormAsync(context.RequestAborted); + if (!CryptographicOperations.FixedTimeEquals(SHA256.HashData(Encoding.UTF8.GetBytes(form["password"].ToString())), SHA256.HashData(Encoding.UTF8.GetBytes(password)))) return Results.Unauthorized(); + // Both identity and session belong to this server configuration, never to the submitted form. + foreach (var prior in applicationSessions.Where(entry => entry.Value <= DateTimeOffset.UtcNow)) applicationSessions.TryRemove(prior.Key, out _); + if (applicationSessions.Count >= 128) return Results.StatusCode(429); + var applicationId = Guid.NewGuid().ToString("N"); applicationSessions[applicationId] = DateTimeOffset.UtcNow.AddMinutes(20); + var identity = new ClaimsIdentity(new[] { new Claim("example_session", applicationId), new Claim(ClaimTypes.NameIdentifier, "example-operator"), new Claim("polymorfa_session", session) }, CookieAuthenticationDefaults.AuthenticationScheme); + await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity)); + context.Response.Headers.CacheControl = "no-store"; return Results.NoContent(); + }); + app.MapPost("/example/logout", async (HttpContext context) => { if (context.Request.Headers.Origin.ToString() != $"{context.Request.Scheme}://{context.Request.Host}") return Results.StatusCode(403); if (context.User.FindFirstValue("example_session") is { } id) applicationSessions.TryRemove(id, out _); await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Results.NoContent(); }); +} +app.MapGet("/example/config", () => Results.Json(new { apiBaseUrl = app.Configuration["POLYMORFA_API_BASE_URL"] ?? "https://api.polymorfa.com" })); +app.MapPost("/api/polymorfa/token", async (HttpContext context, MessagingClient client) => +{ + if (context.Request.Headers.Origin.ToString() != $"{context.Request.Scheme}://{context.Request.Host}") return Results.StatusCode(403); + var subject = context.User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrEmpty(subject)) return Results.StatusCode(403); + context.Response.Headers.CacheControl = "no-store"; + try + { + var response = await client.ClientTokens.MintAsync(new("example:" + subject, Session: session, TtlSeconds: 300), new() { CancellationToken = context.RequestAborted }); + if (!response.Data.Data.Token.StartsWith("pmfa_ct_", StringComparison.Ordinal) || response.Data.Data.Token.Length <= 8 || !DateTimeOffset.TryParse(response.Data.Data.ExpiresAt, out var expiry)) return Results.StatusCode(502); + return Results.Json(new { value = response.Data.Data.Token, audience = "browser", expiresAt = expiry.ToUnixTimeMilliseconds(), scopes = new[] { "read_messages", "connect_whatsapp" }, grant = new { session, conversations = Array.Empty(), allow = new[] { "read_messages", "connect_whatsapp" } } }); + } + catch (PolymorfaException error) { return Results.StatusCode(error.Status is >= 400 and < 500 ? 403 : 502); } +}).RequireAuthorization("PolymorfaSession"); +app.MapGet("/api/polymorfa/conversation", (HttpContext context) => { context.Response.Headers.CacheControl = "no-store"; return Results.Json(conversation.Snapshot()); }).RequireAuthorization("PolymorfaSession"); +app.MapPost("/api/polymorfa/send", async (HttpContext context, MessagingClient client) => +{ + if (context.Request.Headers.Origin.ToString() != $"{context.Request.Scheme}://{context.Request.Host}") return Results.StatusCode(403); + if (context.Request.ContentLength > 16384) return Results.StatusCode(413); + using var body = new MemoryStream(); var buffer = new byte[4096]; + while (true) { var count = await context.Request.Body.ReadAsync(buffer, context.RequestAborted); if (count == 0) break; if (body.Length + count > 16384) return Results.StatusCode(413); await body.WriteAsync(buffer.AsMemory(0, count), context.RequestAborted); } + context.Response.Headers.CacheControl = "no-store"; + return await conversation.SendAsync(client, body.ToArray(), context.Request.Headers["Idempotency-Key"].ToString(), context.RequestAborted); +}).RequireAuthorization("PolymorfaSession"); +// Separate browser-grant examples retain their original empty source. The native inbox uses /conversation. +app.MapGet("/api/polymorfa/history/conversations", (HttpContext context) => { context.Response.Headers.CacheControl = "no-store"; return Results.Json(new { data = Array.Empty(), nextCursor = (string?)null }); }).RequireAuthorization("PolymorfaSession"); +app.MapPost("/api/polymorfa/connect", async (HttpContext context, MessagingClient client) => +{ + if (context.Request.Headers.Origin.ToString() != $"{context.Request.Scheme}://{context.Request.Host}") return Results.StatusCode(403); + context.Response.Headers.CacheControl = "no-store"; + try { var response = await client.QuickLinks.CreateAsync(new(Purpose: "add_connection", Session: session, AddConnection: "linked_devices", ConnectionGoal: "hybrid"), new() { CancellationToken = context.RequestAborted }); return Results.Json(new { data = new { response.Data.Data.Id, response.Data.Data.Url, response.Data.Data.ExpiresAt } }); } + catch (PolymorfaException error) { return Results.StatusCode(error.Status is >= 400 and < 500 ? 403 : 502); } +}).RequireAuthorization("PolymorfaSession"); +app.MapPost("/webhooks/polymorfa", async (HttpContext context) => +{ + if (context.Request.ContentLength > 65536) return Results.StatusCode(413); + using var body = new MemoryStream(); var buffer = new byte[16384]; + while (true) { var count = await context.Request.Body.ReadAsync(buffer, context.RequestAborted); if (count == 0) break; if (body.Length + count > 65536) return Results.StatusCode(413); await body.WriteAsync(buffer.AsMemory(0, count), context.RequestAborted); } + var secret = app.Configuration["POLYMORFA_WEBHOOK_SECRET"]; + if (string.IsNullOrEmpty(secret)) return Results.StatusCode(503); + try { var verified = Webhooks.ConstructEvent(body.ToArray(), context.Request.Headers["x-webhook-signature"].ToString(), secret); conversation.Receive(verified); return Results.NoContent(); } + catch (WebhookSignatureException) { return Results.StatusCode(401); } + catch (PolymorfaValidationException) { return Results.BadRequest(); } + catch (OverflowException) { return Results.BadRequest(); } +}); +app.Run(); diff --git a/packages/dotnet/examples/aspnet-core/wwwroot/elements.ts b/packages/dotnet/examples/aspnet-core/wwwroot/elements.ts new file mode 100644 index 00000000..dd5e3ba5 --- /dev/null +++ b/packages/dotnet/examples/aspnet-core/wwwroot/elements.ts @@ -0,0 +1,70 @@ +import { definePolymorfaElements, type PolymorfaInboxElement } from "@polymorfa/elements"; +import type { ConversationMessage, ConversationEvent, InboxChange, InboxConversation } from "@polymorfa/browser"; + +// `receipt` is the C# host's raw status, shown in #native-status. Only receipts observed from +// message.ack become chat receipts; the send response status is acceptance, never delivery or read. +interface Message extends Omit { receipt: string } +const observedReceipts = new Map([["delivered", "delivered"], ["delivery", "delivered"], ["read", "read"], ["played", "played"]]); +const view = ({ receipt, ...message }: Message): ConversationMessage => { + const state = message.direction === "outbound" ? observedReceipts.get(receipt) : undefined; + return state ? { ...message, receipt: { state } } : message; +}; +interface Snapshot { messages: Message[]; peer: string; session: string; receivedEvents: number; lastEvent: string; localTest: boolean } +definePolymorfaElements(); +export async function mount() { + const inbox = document.querySelector("pmfa-inbox"); + if (!inbox) throw new Error("The inbox is not mounted."); + const changes = new Set<(value: InboxChange) => void>(); + const messages = new Set<(value: ConversationEvent) => void>(); + let previous = "", stopped = false; + let timer: ReturnType | undefined; + const request = async (path: string, options: RequestInit = {}): Promise => { + const response = await fetch(`/api/polymorfa/${path}`, { credentials: "same-origin", ...options }); + if (!response.ok) throw new Error("The application request failed. Check your sign-in and retry."); + return response.json() as Promise; + }; + const row = (snapshot: Snapshot): InboxConversation => { + const last = snapshot.messages.at(-1); + return { id: snapshot.peer, session: snapshot.session, name: "Assigned recipient", phoneNumber: snapshot.peer, + lastActivity: last?.createdAt ?? 0, unreadCount: 0, + ...(last ? {lastMessage:{text:last.text,createdAt:last.createdAt,direction:last.direction}} : {}) }; + }; + const poll = async () => { + try { + const snapshot = await request("conversation"); + if (stopped) return; + const serialized = JSON.stringify(snapshot); + if (serialized !== previous) { + previous = serialized; + for (const notify of changes) notify({type:"conversation",conversation:row(snapshot)}); + for (const message of snapshot.messages) for (const notify of messages) notify({type:"upsert",message:view(message)}); + } + const label = document.querySelector("#transport-label"); + if (label) label.textContent = snapshot.localTest ? "Local test numbers. Simulated WhatsApp transport." : ""; + const status = document.querySelector("#native-status"); + if (status) { + const outgoing = [...snapshot.messages].reverse().find(m => m.direction === "outbound"); + status.textContent = `C# SDK · ${snapshot.receivedEvents} verified webhook events` + (outgoing ? ` · Latest send: ${outgoing.receipt}` : ""); + } + } catch (error) { + const status = document.querySelector("#native-status"); + if (status && !stopped) status.textContent = error instanceof Error ? error.message : "The conversation is unavailable."; + } finally { if (!stopped) timer = setTimeout(poll, 800); } + }; + inbox.source = { + async listConversations(_cursor, signal) { return {conversations:[row(await request("conversation", {...(signal ? {signal} : {})}))]}; }, + subscribe(notify) {changes.add(notify);return () => changes.delete(notify);}, + conversation() { + return { + async load(_cursor, signal) {return {messages:(await request("conversation",{...(signal ? {signal} : {})})).messages.map(view)};}, + subscribe(notify) {messages.add(notify);return () => messages.delete(notify);}, + async send(message, signal) { + if (message.replyTo || message.attachments?.length) throw new Error("This native example sends text only."); + return view(await request("send", {method:"POST",...(signal ? {signal} : {}),headers:{"Content-Type":"application/json","Idempotency-Key":message.clientId},body:JSON.stringify({text:message.text})})); + }, + }; + }, + }; + void poll(); + return {dispose() {stopped=true;clearTimeout(timer);changes.clear();messages.clear();inbox.source=undefined;}}; +} diff --git a/packages/dotnet/examples/aspnet-core/wwwroot/index.html b/packages/dotnet/examples/aspnet-core/wwwroot/index.html new file mode 100644 index 00000000..f84baad6 --- /dev/null +++ b/packages/dotnet/examples/aspnet-core/wwwroot/index.html @@ -0,0 +1,40 @@ + + + +Polymorfa ASP.NET example + +
+

Messaging workspace

+

+ +
+ + + diff --git a/packages/dotnet/examples/blazor-components/Messaging.razor b/packages/dotnet/examples/blazor-components/Messaging.razor new file mode 100644 index 00000000..62d0ab61 --- /dev/null +++ b/packages/dotnet/examples/blazor-components/Messaging.razor @@ -0,0 +1,22 @@ +@namespace Polymorfa.Example.Components +@using Microsoft.JSInterop +@implements IAsyncDisposable +@inject IJSRuntime JavaScript + + + +@code { + [Microsoft.AspNetCore.Components.Parameter] public string TokenEndpoint { get; set; } = "/api/polymorfa/token"; + private IJSObjectReference? module; + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) return; + module = await JavaScript.InvokeAsync("import", "./_content/Polymorfa.Example.Components/messaging.js"); + await module.InvokeVoidAsync("mount", TokenEndpoint); + } + public async ValueTask DisposeAsync() + { + if (module is null) return; + try { await module.InvokeVoidAsync("dispose"); await module.DisposeAsync(); } catch (JSDisconnectedException) { } + } +} diff --git a/packages/dotnet/examples/blazor-components/Polymorfa.Example.Components.csproj b/packages/dotnet/examples/blazor-components/Polymorfa.Example.Components.csproj new file mode 100644 index 00000000..6529af5b --- /dev/null +++ b/packages/dotnet/examples/blazor-components/Polymorfa.Example.Components.csproj @@ -0,0 +1,4 @@ + + net8.0enableenabletrue + + diff --git a/packages/dotnet/examples/blazor-components/wwwroot/messaging.ts b/packages/dotnet/examples/blazor-components/wwwroot/messaging.ts new file mode 100644 index 00000000..957a94e2 --- /dev/null +++ b/packages/dotnet/examples/blazor-components/wwwroot/messaging.ts @@ -0,0 +1,32 @@ +import { + definePolymorfa, + type PolymorfaSessionStatusElement, + type PolymorfaConnectWhatsAppElement, + type PolymorfaInboxElement, +} from "@polymorfa/elements"; +import { PolymorfaClient } from "@polymorfa/browser"; +let client: ReturnType | undefined; +export async function mount(tokenEndpoint: string) { + client?.dispose(); + const response = await fetch("/example/config", { + credentials: "same-origin", + }); + if (!response.ok) + throw new Error("Application configuration could not be loaded."); + const config = (await response.json()) as { apiBaseUrl: string }; + client = definePolymorfa({ + client: new PolymorfaClient({ tokenEndpoint, baseUrl: config.apiBaseUrl }), + }); + for (const element of document.querySelectorAll< + | PolymorfaSessionStatusElement + | PolymorfaConnectWhatsAppElement + | PolymorfaInboxElement + >("pmfa-session-status,pmfa-connect-whatsapp,pmfa-inbox")) + element.client = client; + const inbox = document.querySelector("pmfa-inbox"); + if (inbox) inbox.source = undefined; +} +export function dispose() { + client?.dispose(); + client = undefined; +} diff --git a/packages/dotnet/examples/blazor-wasm/App.razor b/packages/dotnet/examples/blazor-wasm/App.razor new file mode 100644 index 00000000..a9a2f2c1 --- /dev/null +++ b/packages/dotnet/examples/blazor-wasm/App.razor @@ -0,0 +1,5 @@ +@namespace Polymorfa.Example.Blazor +@using Polymorfa.Example.Components +

Polymorfa Blazor WebAssembly example

+

Sign in through the ASP.NET application before opening this example.

+ diff --git a/packages/dotnet/examples/blazor-wasm/Polymorfa.Example.Blazor.csproj b/packages/dotnet/examples/blazor-wasm/Polymorfa.Example.Blazor.csproj new file mode 100644 index 00000000..5e084d64 --- /dev/null +++ b/packages/dotnet/examples/blazor-wasm/Polymorfa.Example.Blazor.csproj @@ -0,0 +1,4 @@ + + net8.0enableenabletrue + + diff --git a/packages/dotnet/examples/blazor-wasm/Program.cs b/packages/dotnet/examples/blazor-wasm/Program.cs new file mode 100644 index 00000000..cdf4e29b --- /dev/null +++ b/packages/dotnet/examples/blazor-wasm/Program.cs @@ -0,0 +1,4 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +await builder.Build().RunAsync(); diff --git a/packages/dotnet/examples/blazor-wasm/wwwroot/index.html b/packages/dotnet/examples/blazor-wasm/wwwroot/index.html new file mode 100644 index 00000000..71227b5c --- /dev/null +++ b/packages/dotnet/examples/blazor-wasm/wwwroot/index.html @@ -0,0 +1 @@ +Polymorfa Blazor WebAssembly example
Loading application…
diff --git a/packages/dotnet/examples/build-elements.mjs b/packages/dotnet/examples/build-elements.mjs new file mode 100644 index 00000000..11872604 --- /dev/null +++ b/packages/dotnet/examples/build-elements.mjs @@ -0,0 +1,17 @@ +import { build } from "esbuild"; +import { fileURLToPath } from "node:url"; +for (const [entry, out] of [ + ["aspnet-core/wwwroot/elements.ts", "aspnet-core/wwwroot/elements.js"], + [ + "blazor-components/wwwroot/messaging.ts", + "blazor-components/wwwroot/messaging.js", + ], +]) + await build({ + entryPoints: [fileURLToPath(new URL(entry, import.meta.url))], + outfile: fileURLToPath(new URL(out, import.meta.url)), + bundle: true, + format: "esm", + platform: "browser", + target: "es2022", + }); diff --git a/packages/dotnet/examples/calls-agent/Polymorfa.Example.Calls.csproj b/packages/dotnet/examples/calls-agent/Polymorfa.Example.Calls.csproj new file mode 100644 index 00000000..eb164114 --- /dev/null +++ b/packages/dotnet/examples/calls-agent/Polymorfa.Example.Calls.csproj @@ -0,0 +1,4 @@ + + Exenet8.0enableenabletrue + + diff --git a/packages/dotnet/examples/calls-agent/Program.cs b/packages/dotnet/examples/calls-agent/Program.cs new file mode 100644 index 00000000..a60f2a09 --- /dev/null +++ b/packages/dotnet/examples/calls-agent/Program.cs @@ -0,0 +1,27 @@ +using Polymorfa.Sdk; + +using var stop = new CancellationTokenSource(); +Console.CancelKeyPress += (_, e) => { e.Cancel = true; stop.Cancel(); }; +var token = Environment.GetEnvironmentVariable("POLYMORFA_API_KEY") ?? throw new InvalidOperationException("POLYMORFA_API_KEY is required."); +var session = Environment.GetEnvironmentVariable("POLYMORFA_SESSION") ?? throw new InvalidOperationException("POLYMORFA_SESSION is required."); +await using var calls = new CallsClient(token, new() { Session = session, Participant = "agent" }); +await calls.ConnectAsync(stop.Token); +try +{ + await foreach (var item in calls.EventsAsync(stop.Token)) + { + if (item is CallIncoming incoming) + { + // Consent and application policy belong before AnswerAsync. + await incoming.Call.AnswerAsync(cancellationToken: stop.Token); + _ = ObserveAudio(incoming.Call, stop.Token); + } + } +} +catch (OperationCanceledException) when (stop.IsCancellationRequested) { } +static async Task ObserveAudio(Call call, CancellationToken cancellation) +{ + try { await foreach (var frame in call.MediaFramesAsync(cancellation)) { if (frame is CallAudioFrame audio) { /* Process signed 16-bit mono PCM at call.SampleRate; never log audio. */ _ = audio.Pcm.Length; } } } + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { } + catch (PolymorfaException) { /* Application-specific failure handling without logging tokens or media. */ } +} diff --git a/packages/dotnet/examples/maui/.gitignore b/packages/dotnet/examples/maui/.gitignore new file mode 100644 index 00000000..59672943 --- /dev/null +++ b/packages/dotnet/examples/maui/.gitignore @@ -0,0 +1,2 @@ +**/bin/ +**/obj/ diff --git a/packages/dotnet/examples/maui/README.md b/packages/dotnet/examples/maui/README.md new file mode 100644 index 00000000..2d710ceb --- /dev/null +++ b/packages/dotnet/examples/maui/README.md @@ -0,0 +1,43 @@ +# MAUI Blazor Hybrid + +This Mac Catalyst application hosts the existing `blazor-components` Razor component and `@polymorfa/elements`. It uses an authenticated application server to mint short-lived client tokens and load inbox history. It contains no server API key. These development sources do not establish registry publication or hosted API availability. + +## Build and run + +Use .NET SDK **10.0.401**, workload set **10.0.401**, MAUI **10.0.20** and Xcode **26.6** on an Apple Silicon Mac. The [official Mac Catalyst release](https://github.com/dotnet/macios/releases/tag/dotnet-10.0.1xx-xcode26.5-10318) supplies pack **26.5.10318** for Xcode 26.6. Workload set `10.0.401.1` targets another Xcode version; use the pinned set below. See the [MAUI Mac Catalyst CLI guide](https://learn.microsoft.com/en-us/dotnet/maui/mac-catalyst/cli?view=net-maui-10.0) for platform setup. + +From the repository root: + +```sh +npm ci +npm run build:workspaces +node packages/dotnet/examples/build-elements.mjs +dotnet workload install maui-maccatalyst --version 10.0.401 +dotnet build packages/dotnet/examples/maui/app/Polymorfa.Example.Maui.csproj -f net10.0-maccatalyst26.5 -c Debug -p:TreatWarningsAsErrors=true +open "packages/dotnet/examples/maui/app/bin/Debug/net10.0-maccatalyst26.5/maccatalyst-arm64/Polymorfa MAUI.app" +``` + +Start the [ASP.NET Core example](../README.md#aspnet-core) at `http://127.0.0.1:58284` with its Development demonstration identity configured before signing in. Its server owns `POLYMORFA_API_KEY`, `POLYMORFA_SESSION` and `POLYMORFA_EXAMPLE_PASSWORD`. Enter only the application password in MAUI. Keep API credentials in server configuration. Replace the demonstration identity with your application's authorization before deployment. + +`app/MauiProgram.cs` sets the trusted application server at build time. Debug uses the loopback address above. Release uses `https://your-application.example/`; replace that placeholder with your application server's HTTPS address before building a release. JavaScript, request bodies and runtime environment variables cannot select another server. The local networking entitlement permits the development loopback host; it does not disable certificate verification or permit arbitrary insecure origins. + +## Authentication bridge + +The native bridge keeps the HttpOnly application cookie in an in-memory `CookieContainer`. It sends the application password with POST and keeps the cookie out of JavaScript. The shared components run at the bundled WebView origin `app://0.0.0.1`, which cannot use the server's Strict SameSite cookie directly. Their fixed config, token, history and connection requests cross the native bridge; the native client supplies the configured server's exact Origin. Server routes retain their authentication and origin checks. + +The bridge passes only a fixed action identifier. Browser-supplied headers, cookies, session identifiers and bodies cannot choose the trusted principal or API scope. Unrelated API requests retain the ordinary browser client transport and its short-lived scoped token. There is no general-purpose URL proxy. Redirects are refused, responses are bounded to 1 MiB, and a 30-second deadline covers both response headers and body. Unknown non-JSON responses become a safe error envelope. + +Sign-out calls the server's session revocation route, clears native cookies and removes the components. A server sign-out failure reports that only local clearing was confirmed. An unauthorized response clears the local session and removes the mounted components. Native cookies are not persisted across app restarts. + +## Verification + +The bridge tests need .NET 8 or later and no MAUI workload: + +```sh +dotnet run --project packages/dotnet/examples/maui/bridge-tests/Polymorfa.Example.Maui.Bridge.Tests.csproj +node packages/dotnet/examples/maui/bridge-tests/native-bridge.test.mjs +``` + +The console tests use actual TCP HTTP responses to check authentication, exact Origin, fixed route admission, cookie isolation, redirects, HTML failures, body limits, cancellation, stalled-body deadlines, revocation and logout. The JavaScript tests check the opaque WebView origin, route and method allowlists, discarded browser fields, cancellation and restoration of the original fetch function. An optional `--live http://127.0.0.1:58284/` argument exercises the local ASP.NET Core example with the synthetic test password `local-example-only`. + +The Mac Catalyst app was built with warnings treated as errors and exercised on macOS 26.6: native sign-in mounted the shared components, obtained a scoped token, loaded the empty synthetic inbox and signed out. The owning ASP.NET server independently confirmed the login, config, token, history and logout requests. This proves the local native host and authentication bridge; it does not prove live WhatsApp or provider connectivity. Windows, Android and iOS hosts are not supplied or exercised. diff --git a/packages/dotnet/examples/maui/app/App.xaml b/packages/dotnet/examples/maui/app/App.xaml new file mode 100644 index 00000000..a80a5a42 --- /dev/null +++ b/packages/dotnet/examples/maui/app/App.xaml @@ -0,0 +1 @@ + diff --git a/packages/dotnet/examples/maui/app/App.xaml.cs b/packages/dotnet/examples/maui/app/App.xaml.cs new file mode 100644 index 00000000..f03ccb26 --- /dev/null +++ b/packages/dotnet/examples/maui/app/App.xaml.cs @@ -0,0 +1,6 @@ +namespace Polymorfa.Example.Maui; +public partial class App : Application +{ + public App() { InitializeComponent(); } + protected override Window CreateWindow(IActivationState? state) => new(new MainPage()) { Title = "Polymorfa MAUI", Width = 1024, Height = 850 }; +} diff --git a/packages/dotnet/examples/maui/app/Home.razor b/packages/dotnet/examples/maui/app/Home.razor new file mode 100644 index 00000000..e55e7846 --- /dev/null +++ b/packages/dotnet/examples/maui/app/Home.razor @@ -0,0 +1,79 @@ +@namespace Polymorfa.Example.Maui +@implements IAsyncDisposable +@inject ApplicationBridge Bridge +@inject IJSRuntime JavaScript +

Polymorfa MAUI

+@if (!signedIn) +{ +

Sign in to the application's local demonstration account.

+
+ + + +
+} +else +{ + +

Application session authenticated

+ +} +

@message

+@code { + private string password = ""; + private string message = ""; + private bool signedIn; + private bool busy; + private IJSObjectReference? module; + private DotNetObjectReference? reference; + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) return; + reference = DotNetObjectReference.Create(this); + module = await JavaScript.InvokeAsync("import", "./native-bridge.js"); + await module.InvokeVoidAsync("install", reference); + StateHasChanged(); + } + private async Task SignIn() + { + busy = true; + message = ""; + try { signedIn = await Bridge.LoginAsync(password); if (!signedIn) message = "Sign-in failed."; } + catch (Exception) { message = "The application server could not be reached."; } + finally { password = ""; busy = false; } + } + private async Task SignOut() + { + busy = true; + try { await Bridge.RequestAsync("logout"); message = "Signed out."; } + catch (Exception) { message = "The local session was cleared; server sign-out could not be confirmed."; } + finally { signedIn = false; busy = false; } + } + [JSInvokable] public async Task RequestApplicationAsync(string action) + { + try + { + var response = await Bridge.RequestAsync(action); + if (response.Status == 401 && signedIn) await InvokeAsync(() => { signedIn = false; message = "Your application session expired."; StateHasChanged(); }); + return response; + } + catch (Exception) { return new(502, "", "application/json"); } + } + public async ValueTask DisposeAsync() + { + try + { + if (module is not null) + { + try { await module.InvokeVoidAsync("dispose"); } + catch (JSException) { /* The stopped WebView has already released its JavaScript handle. */ } + finally + { + try { await module.DisposeAsync(); } + catch (JSException) { /* Cleanup after native WebView shutdown. */ } + } + } + } + finally { reference?.Dispose(); } + } +} diff --git a/packages/dotnet/examples/maui/app/MainPage.xaml b/packages/dotnet/examples/maui/app/MainPage.xaml new file mode 100644 index 00000000..19483afb --- /dev/null +++ b/packages/dotnet/examples/maui/app/MainPage.xaml @@ -0,0 +1,4 @@ + + + + diff --git a/packages/dotnet/examples/maui/app/MainPage.xaml.cs b/packages/dotnet/examples/maui/app/MainPage.xaml.cs new file mode 100644 index 00000000..d08f7c15 --- /dev/null +++ b/packages/dotnet/examples/maui/app/MainPage.xaml.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Components.WebView; +namespace Polymorfa.Example.Maui; +public partial class MainPage : ContentPage +{ + public MainPage() { InitializeComponent(); } + private void OnUrlLoading(object? sender, UrlLoadingEventArgs e) + { + // Bundled UI is the only content allowed to run with the native app bridge. + if (e.Url.Host != "0.0.0.1" || e.Url.Scheme != "app") e.UrlLoadingStrategy = UrlLoadingStrategy.OpenExternally; + } +} diff --git a/packages/dotnet/examples/maui/app/MauiProgram.cs b/packages/dotnet/examples/maui/app/MauiProgram.cs new file mode 100644 index 00000000..566f5e1d --- /dev/null +++ b/packages/dotnet/examples/maui/app/MauiProgram.cs @@ -0,0 +1,18 @@ +using Polymorfa.Example.Maui.Bridge; +namespace Polymorfa.Example.Maui; +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder().UseMauiApp(); + builder.Services.AddMauiBlazorWebView(); + // These are build-owned addresses; JavaScript and runtime environment cannot repoint a signed app. +#if DEBUG + var applicationServer = new Uri("http://127.0.0.1:58284/"); +#else + var applicationServer = new Uri("https://your-application.example/"); +#endif + builder.Services.AddSingleton(new ApplicationBridge(applicationServer)); + return builder.Build(); + } +} diff --git a/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/AppDelegate.cs b/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 00000000..f89edf16 --- /dev/null +++ b/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,4 @@ +using Foundation; +namespace Polymorfa.Example.Maui; +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate { protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); } diff --git a/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/Entitlements.plist b/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/Entitlements.plist new file mode 100644 index 00000000..cb0ca0e8 --- /dev/null +++ b/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/Entitlements.plist @@ -0,0 +1 @@ +com.apple.security.app-sandboxcom.apple.security.network.client diff --git a/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/Info.plist b/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/Info.plist new file mode 100644 index 00000000..29e97afe --- /dev/null +++ b/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/Info.plist @@ -0,0 +1 @@ +UIDeviceFamily2UISupportedInterfaceOrientationsUIInterfaceOrientationPortraitUIInterfaceOrientationLandscapeLeftUIInterfaceOrientationLandscapeRightNSAppTransportSecurityNSAllowsLocalNetworking diff --git a/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/Program.cs b/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/Program.cs new file mode 100644 index 00000000..cab7b8ce --- /dev/null +++ b/packages/dotnet/examples/maui/app/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,3 @@ +using UIKit; +namespace Polymorfa.Example.Maui; +public class Program { static void Main(string[] args) => UIApplication.Main(args, null, typeof(AppDelegate)); } diff --git a/packages/dotnet/examples/maui/app/Polymorfa.Example.Maui.csproj b/packages/dotnet/examples/maui/app/Polymorfa.Example.Maui.csproj new file mode 100644 index 00000000..3e62e08e --- /dev/null +++ b/packages/dotnet/examples/maui/app/Polymorfa.Example.Maui.csproj @@ -0,0 +1,15 @@ + + + net10.0-maccatalyst26.5ExePolymorfa.Example.Maui + truetrueenableenablefalse + Polymorfa MAUIcom.polymorfa.sdkexample.maui1.01 + 15.0maccatalyst-arm64 + 10.0.20-Platforms/MacCatalyst/Entitlements.plist + + + + + + + + diff --git a/packages/dotnet/examples/maui/app/_Imports.razor b/packages/dotnet/examples/maui/app/_Imports.razor new file mode 100644 index 00000000..388d2ef4 --- /dev/null +++ b/packages/dotnet/examples/maui/app/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using Polymorfa.Example.Components +@using Polymorfa.Example.Maui.Bridge diff --git a/packages/dotnet/examples/maui/app/wwwroot/app.css b/packages/dotnet/examples/maui/app/wwwroot/app.css new file mode 100644 index 00000000..f66c2e86 --- /dev/null +++ b/packages/dotnet/examples/maui/app/wwwroot/app.css @@ -0,0 +1 @@ +body { font: 16px system-ui, sans-serif; margin: 24px; color: #182434; background: #f8fafc; } main { max-width: 960px; margin: auto; } form { display: grid; gap: 12px; max-width: 420px; } input, button { min-height: 44px; font: inherit; border: 1px solid #50647a; border-radius: 8px; padding: 6px 12px; } button { cursor: pointer; background: #1d4ed8; color: white; } input:focus-visible, button:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; } pmfa-session-status, pmfa-connect-whatsapp, pmfa-inbox { display: block; margin-top: 20px; } diff --git a/packages/dotnet/examples/maui/app/wwwroot/index.html b/packages/dotnet/examples/maui/app/wwwroot/index.html new file mode 100644 index 00000000..1ce185f4 --- /dev/null +++ b/packages/dotnet/examples/maui/app/wwwroot/index.html @@ -0,0 +1 @@ +Polymorfa MAUI
Loading application…
diff --git a/packages/dotnet/examples/maui/app/wwwroot/native-bridge.js b/packages/dotnet/examples/maui/app/wwwroot/native-bridge.js new file mode 100644 index 00000000..762e89a6 --- /dev/null +++ b/packages/dotnet/examples/maui/app/wwwroot/native-bridge.js @@ -0,0 +1,31 @@ +// Fixed app routes only; remote Polymorfa API requests keep the browser client's scoped token. +const routes = new Map([ + ["/example/config", ["GET", "config"]], + ["/api/polymorfa/token", ["POST", "token"]], + ["/api/polymorfa/history/conversations", ["GET", "conversations"]], + ["/api/polymorfa/connect", ["POST", "connect"]], +]); +let originalFetch; +export function install(native) { + if (originalFetch) throw new Error("The native bridge is already installed."); + originalFetch = globalThis.fetch.bind(globalThis); + globalThis.fetch = async (input, options) => { + const url = new URL(typeof input === "string" ? input : input.url ?? input.href, location.href); + const route = url.protocol === new URL(location.href).protocol && url.host === new URL(location.href).host && !url.username && !url.password && !url.search && !url.hash ? routes.get(url.pathname) : undefined; + if (!route) return originalFetch(input, options); + const method = (options?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase(); + if (method !== route[0]) return new Response(null, { status: 405 }); + const signal = options?.signal ?? (input instanceof Request ? input.signal : undefined); + if (signal?.aborted) throw new DOMException("Aborted", "AbortError"); + const response = await native.invokeMethodAsync("RequestApplicationAsync", route[1]); + if (signal?.aborted) throw new DOMException("Aborted", "AbortError"); + return new Response(response.status === 204 ? null : response.body, { + status: response.status, + headers: { "Content-Type": response.contentType, "Cache-Control": "no-store" }, + }); + }; +} +export function dispose() { + if (originalFetch) globalThis.fetch = originalFetch; + originalFetch = undefined; +} diff --git a/packages/dotnet/examples/maui/bridge-tests/Polymorfa.Example.Maui.Bridge.Tests.csproj b/packages/dotnet/examples/maui/bridge-tests/Polymorfa.Example.Maui.Bridge.Tests.csproj new file mode 100644 index 00000000..231b808a --- /dev/null +++ b/packages/dotnet/examples/maui/bridge-tests/Polymorfa.Example.Maui.Bridge.Tests.csproj @@ -0,0 +1 @@ +net8.0Exeenableenabletrue diff --git a/packages/dotnet/examples/maui/bridge-tests/Program.cs b/packages/dotnet/examples/maui/bridge-tests/Program.cs new file mode 100644 index 00000000..0c88dfdd --- /dev/null +++ b/packages/dotnet/examples/maui/bridge-tests/Program.cs @@ -0,0 +1,130 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using Polymorfa.Example.Maui.Bridge; + +static void Check(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); } +foreach (var url in new[] { "http://example.com/", "https://user:password@example.com/", "https://example.com/path", "https://example.com/?x=1", "https://example.com/#fragment", "file:///" }) +{ + try { using var invalid = new ApplicationBridge(new(url)); throw new Exception("Unsafe URL accepted"); } + catch (ArgumentException) { } +} +using var listener = new TcpListener(IPAddress.Loopback, 0); +listener.Start(); +var origin = new Uri($"http://127.0.0.1:{((IPEndPoint)listener.LocalEndpoint).Port}/"); +using var stop = new CancellationTokenSource(); +var requests = new List<(string Method, string Path, Dictionary Headers, string Body)>(); +var redirect = false; +var oversized = false; +var unauthorized = false; +var delay = false; +var html = false; +var server = Task.Run(async () => +{ + try + { + while (!stop.IsCancellationRequested) + { + using var socket = await listener.AcceptTcpClientAsync(stop.Token); + await using var stream = socket.GetStream(); + using var reader = new StreamReader(stream, Encoding.UTF8, false, 1024, leaveOpen: true); + var first = (await reader.ReadLineAsync(stop.Token))!.Split(' '); + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + while (await reader.ReadLineAsync(stop.Token) is { Length: > 0 } line) { var colon = line.IndexOf(':'); headers[line[..colon]] = line[(colon + 1)..].Trim(); } + var bodyChars = new char[headers.TryGetValue("Content-Length", out var size) ? int.Parse(size) : 0]; + if (bodyChars.Length > 0) await reader.ReadBlockAsync(bodyChars, stop.Token); + var body = new string(bodyChars); + lock (requests) requests.Add((first[0], first[1], headers, body)); + var status = 200; + var extra = ""; + var content = "{}"; + if (first[1] == "/example/login") + { + Check(first[0] == "POST" && headers.GetValueOrDefault("Origin") == origin.GetLeftPart(UriPartial.Authority), "Native login origin/method"); + status = body == "password=fixture-only" ? 204 : 401; + if (status == 204) extra = "Set-Cookie: app-session=opaque-fixture; HttpOnly; SameSite=Strict; Path=/\r\n"; + content = ""; + } + else if (first[1] == "/example/config") content = "{\"apiBaseUrl\":\"https://api.polymorfa.com\"}"; + else + { + Check(headers.GetValueOrDefault("Cookie") == "app-session=opaque-fixture", "Application cookie must remain native and attached only to trusted host"); + if (first[0] == "POST") Check(headers.GetValueOrDefault("Origin") == origin.GetLeftPart(UriPartial.Authority), "Exact app-owned Origin"); + if (unauthorized) status = 401; + else if (redirect) { status = 302; extra = "Location: https://example.com/steal\r\n"; } + else if (first[1] == "/api/polymorfa/token") content = "{\"value\":\"pmfa_ct_fixture_only\",\"audience\":\"browser\"}"; + else if (first[1] == "/api/polymorfa/history/conversations") content = "{\"data\":[],\"nextCursor\":null}"; + else if (first[1] == "/example/logout") { status = 204; content = ""; } + } + Check(!headers.ContainsKey("Authorization") && !body.Contains("pmfa_at_") && !body.Contains("pmfa_pt_"), "Device must not contain or transmit server SDK credentials"); + if (html) content = "proxy error with private operator information"; + var contentType = html ? "text/html" : "application/json"; + var bytes = Encoding.UTF8.GetBytes(content); + var length = oversized ? 1048577 : bytes.Length; + await stream.WriteAsync(Encoding.ASCII.GetBytes($"HTTP/1.1 {status} Fixture\r\nContent-Type: {contentType}\r\nContent-Length: {length}\r\nConnection: close\r\n{extra}\r\n"), stop.Token); + if (delay) await Task.Delay(300, stop.Token); + await stream.WriteAsync(bytes, stop.Token); + } + } + catch (OperationCanceledException) { } +}); +try +{ + using var bridge = new ApplicationBridge(origin); + Check((await bridge.RequestAsync("token")).Status == 401 && requests.Count == 0, "No unauthenticated token request"); + Check(!await bridge.LoginAsync("wrong") && !bridge.IsAuthenticated, "Invalid application sign-in"); + Check(await bridge.LoginAsync("fixture-only") && bridge.IsAuthenticated, "Native cookie sign-in"); + var config = await bridge.RequestAsync("config"); + Check(config.Status == 200 && config.Body.Contains("apiBaseUrl"), "Trusted API config"); + var grant = await bridge.RequestAsync("token"); + Check(grant.Status == 200 && grant.Body.Contains("pmfa_ct_fixture_only") && !JsonSerializer.Serialize(grant).Contains("opaque-fixture"), "Only scoped browser grant crosses bridge"); + Check((await bridge.RequestAsync("conversations")).Body.Contains("nextCursor"), "App-owned authorized history"); + Check((await bridge.RequestAsync("connect")).Status == 200, "App-owned authorized connect"); + var before = requests.Count; + foreach (var action in new[] { "https://example.com/", "../token", "token?session=foreign", "/api/polymorfa/token", "Token" }) + { + try { await bridge.RequestAsync(action); throw new Exception("Unknown action accepted"); } catch (ArgumentException) { } + } + Check(requests.Count == before, "Unknown paths rejected before network"); + redirect = true; + Check((await bridge.RequestAsync("token")).Status == 502, "Redirect must not leak native cookie"); + redirect = false; html = true; + var invalidBody = await bridge.RequestAsync("token"); + Check(invalidBody.Status == 502 && invalidBody.Body.Contains("invalid_response") && !invalidBody.Body.Contains("private operator"), "HTML upstream response must become safe error"); + html = false; oversized = true; + try { await bridge.RequestAsync("token"); throw new Exception("Oversized response accepted"); } catch (InvalidDataException) { } + oversized = false; delay = true; + using (var cancellation = new CancellationTokenSource(50)) + { try { await bridge.RequestAsync("token", cancellation.Token); throw new Exception("Cancellation ignored"); } catch (OperationCanceledException) { } } + delay = false; + await Task.Delay(350); + using (var shortBridge = new ApplicationBridge(origin, TimeSpan.FromMilliseconds(100))) + { + Check(await shortBridge.LoginAsync("fixture-only"), "Bounded bridge login"); + delay = true; + var started = DateTimeOffset.UtcNow; + try { await shortBridge.RequestAsync("token"); throw new Exception("Header-only deadline ignored body stall"); } catch (OperationCanceledException) { } + Check(DateTimeOffset.UtcNow - started < TimeSpan.FromSeconds(1), "Deadline covers delayed response body"); + delay = false; + await Task.Delay(350); + } + unauthorized = true; + Check((await bridge.RequestAsync("token")).Status == 401 && !bridge.IsAuthenticated, "Server revocation clears native auth"); + unauthorized = false; + Check(await bridge.LoginAsync("fixture-only"), "Fresh app sign-in"); + Check((await bridge.RequestAsync("logout")).Status == 204 && !bridge.IsAuthenticated, "Server and native logout"); + before = requests.Count; + Check((await bridge.RequestAsync("token")).Status == 401 && requests.Count == before, "Logout fences future native token reads"); + Console.WriteLine("Native TCP app auth/cookie/origin/grant/history/connect/revocation/logout, URL confinement, redirect/size/cancellation checks passed."); +} +finally { stop.Cancel(); listener.Stop(); await server; } +if (args is ["--live", var liveUrl]) +{ + using var live = new ApplicationBridge(new(liveUrl)); + Check(await live.LoginAsync("local-example-only"), "Live ASP app login"); + Check((await live.RequestAsync("token")).Status == 200, "Live ASP scoped grant"); + Check((await live.RequestAsync("conversations")).Status == 200, "Live ASP app history"); + Check((await live.RequestAsync("logout")).Status == 204 && (await live.RequestAsync("token")).Status == 401, "Live ASP logout boundary"); + Console.WriteLine("Live ASP native cookie bridge login/token/history/logout passed."); +} diff --git a/packages/dotnet/examples/maui/bridge-tests/native-bridge.test.mjs b/packages/dotnet/examples/maui/bridge-tests/native-bridge.test.mjs new file mode 100644 index 00000000..8120bb45 --- /dev/null +++ b/packages/dotnet/examples/maui/bridge-tests/native-bridge.test.mjs @@ -0,0 +1,28 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import vm from "node:vm"; +const source = fs.readFileSync(new URL("../app/wwwroot/native-bridge.js", import.meta.url), "utf8").replaceAll("export function", "function"); +const forwarded = []; +const bridged = []; +const context = vm.createContext({ URL, Request, Response, DOMException, location: { href: "https://0.0.0.1/", origin: "https://0.0.0.1" }, fetch: async (...args) => { forwarded.push(args); return new Response("{}"); } }); +vm.runInContext(source + ";globalThis.testInstall=install;globalThis.testDispose=dispose", context); +context.testInstall({ invokeMethodAsync: async (_, action) => { bridged.push(action); return { status: 200, body: "{}", contentType: "application/json" }; } }); +assert.equal((await context.fetch("/api/polymorfa/token", { method: "POST", headers: { Origin: "https://evil.example", Cookie: "stolen" }, body: '{"session":"foreign"}' })).status, 200); +assert.equal(bridged.join(","), "token"); +assert.equal((await context.fetch("/api/polymorfa/token")).status, 405); +await context.fetch("https://api.polymorfa.com/messaging/support/status", { headers: { Authorization: "Bearer pmfa_ct_Zml4dHVyZQ" } }); +assert.equal(forwarded.length, 1); +await context.fetch("/api/polymorfa/token?session=foreign", { method: "POST" }); +assert.equal(bridged.length, 1); +const abort = new AbortController(); abort.abort(); +await assert.rejects(context.fetch("/example/config", { signal: abort.signal }), { name: "AbortError" }); +await context.fetch("app://evil.example/api/polymorfa/token", { method: "POST" }); +assert.equal(bridged.length, 1); +context.testDispose(); await context.fetch("/example/config"); +assert.equal(forwarded.length, 4); +context.location.href = "app://0.0.0.1/"; context.location.origin = "null"; +context.testInstall({ invokeMethodAsync: async (_, action) => { bridged.push(action); return {status: 200, body: "{}", contentType: "application/json"}; } }); +await context.fetch("/example/config"); assert.equal(bridged.at(-1), "config"); +await context.fetch("app://evil.example/api/polymorfa/token", {method: "POST"}); assert.equal(bridged.at(-1), "config"); +context.testDispose(); +console.log("Actual bridge module route/method/header/body isolation, cancellation and disposal checks passed."); diff --git a/packages/dotnet/examples/maui/bridge/ApplicationBridge.cs b/packages/dotnet/examples/maui/bridge/ApplicationBridge.cs new file mode 100644 index 00000000..c1779ec0 --- /dev/null +++ b/packages/dotnet/examples/maui/bridge/ApplicationBridge.cs @@ -0,0 +1,118 @@ +using System.Net; +using System.Text.Json; + +namespace Polymorfa.Example.Maui.Bridge; + +public sealed record BridgeResponse(int Status, string Body, string ContentType); + +/// App authentication lives in native memory, separate from WebView cookies. +public sealed class ApplicationBridge : IDisposable +{ + private readonly HttpClient client; + private readonly HttpClientHandler handler; + private readonly Uri server; + private readonly TimeSpan requestTimeout; + private readonly CancellationTokenSource lifetime = new(); + private readonly SemaphoreSlim authentication = new(1, 1); + public bool IsAuthenticated { get; private set; } + public Uri Server => server; + + public ApplicationBridge(Uri trustedServer, TimeSpan? timeout = null) + { + if (!trustedServer.IsAbsoluteUri || trustedServer.UserInfo.Length != 0 || trustedServer.Query.Length != 0 || trustedServer.Fragment.Length != 0 || trustedServer.AbsolutePath != "/" || + (trustedServer.Scheme != "https" && !(trustedServer.Scheme == "http" && trustedServer.IsLoopback))) + throw new ArgumentException("The application server must be HTTPS, or loopback HTTP for development, with no path or credentials.", nameof(trustedServer)); + requestTimeout = timeout ?? TimeSpan.FromSeconds(30); + if (requestTimeout <= TimeSpan.Zero || requestTimeout > TimeSpan.FromSeconds(30)) throw new ArgumentOutOfRangeException(nameof(timeout)); + server = trustedServer; + handler = new HttpClientHandler { CookieContainer = new CookieContainer(), UseCookies = true, AllowAutoRedirect = false }; + client = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan }; + } + + public async Task LoginAsync(string password, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(password) || System.Text.Encoding.UTF8.GetByteCount(password) > 1024) return false; + using var linked = CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token, cancellationToken); + await authentication.WaitAsync(linked.Token); + try + { + ClearCookies(); + using var request = Request(HttpMethod.Post, "/example/login"); + request.Content = new FormUrlEncodedContent(new Dictionary { ["password"] = password }); + var response = await SendAsync(request, linked.Token); + IsAuthenticated = response.Status == 204; + if (!IsAuthenticated) ClearCookies(); + return IsAuthenticated; + } + finally { authentication.Release(); } + } + + /// Fixed app-owned routes. This deliberately is not a URL/header/body proxy. + public async Task RequestAsync(string action, CancellationToken cancellationToken = default) + { + var (method, path) = action switch + { + "config" => (HttpMethod.Get, "/example/config"), + "token" => (HttpMethod.Post, "/api/polymorfa/token"), + "conversations" => (HttpMethod.Get, "/api/polymorfa/history/conversations"), + "connect" => (HttpMethod.Post, "/api/polymorfa/connect"), + "logout" => (HttpMethod.Post, "/example/logout"), + _ => throw new ArgumentException("Unknown application route.", nameof(action)) + }; + if (!IsAuthenticated && action is not "config" and not "logout") return new(401, "", "application/json"); + using var linked = CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token, cancellationToken); + using var request = Request(method, path); + try + { + var response = await SendAsync(request, linked.Token); + if (response.Status == 401) ClearCookies(); + if (action == "config" && response.Status == 200) + { + using var json = JsonDocument.Parse(response.Body); + if (!json.RootElement.TryGetProperty("apiBaseUrl", out var value) || !Uri.TryCreate(value.GetString(), UriKind.Absolute, out var uri) || + (uri.Scheme != "https" && !(uri.Scheme == "http" && uri.IsLoopback)) || uri.UserInfo.Length != 0 || uri.Fragment.Length != 0 || uri.Query.Length != 0) + throw new InvalidDataException("The application returned an invalid API URL."); + } + return response; + } + finally { if (action == "logout") ClearCookies(); } + } + + private HttpRequestMessage Request(HttpMethod method, string path) + { + var request = new HttpRequestMessage(method, new Uri(server, path)); + if (method == HttpMethod.Post) request.Headers.Add("Origin", server.GetLeftPart(UriPartial.Authority)); + request.Headers.Add("Accept", "application/json"); + return request; + } + + private async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + deadline.CancelAfter(requestTimeout); + cancellationToken = deadline.Token; + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + if (response.StatusCode == HttpStatusCode.Unauthorized) ClearCookies(); + // Do not follow redirects with app authentication or expose headers/cookies to JavaScript. + if ((int)response.StatusCode is >= 300 and < 400) return new(502, "", "application/json"); + const int limit = 1024 * 1024; + if (response.Content.Headers.ContentLength > limit) throw new InvalidDataException("Application response exceeded the byte limit."); + using var body = new MemoryStream(); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + var buffer = new byte[16384]; + while (true) + { + var count = await stream.ReadAsync(buffer, cancellationToken); + if (count == 0) break; + if (body.Length + count > limit) throw new InvalidDataException("Application response exceeded the byte limit."); + await body.WriteAsync(buffer.AsMemory(0, count), cancellationToken); + } + var contentType = response.Content.Headers.ContentType?.MediaType ?? ""; + if (body.Length > 0 && !contentType.Equals("application/json", StringComparison.OrdinalIgnoreCase) && !contentType.EndsWith("+json", StringComparison.OrdinalIgnoreCase)) + return new(502, "{\"error\":{\"code\":\"invalid_response\",\"message\":\"The application server returned an invalid response.\"}}", "application/json"); + return new((int)response.StatusCode, System.Text.Encoding.UTF8.GetString(body.ToArray()), contentType.Length == 0 ? "application/json" : contentType); + } + + private void ClearCookies() { foreach (Cookie cookie in handler.CookieContainer.GetAllCookies()) cookie.Expired = true; IsAuthenticated = false; } + public void Dispose() { lifetime.Cancel(); client.Dispose(); lifetime.Dispose(); } +} diff --git a/packages/dotnet/examples/maui/bridge/Polymorfa.Example.Maui.Bridge.csproj b/packages/dotnet/examples/maui/bridge/Polymorfa.Example.Maui.Bridge.csproj new file mode 100644 index 00000000..a016018f --- /dev/null +++ b/packages/dotnet/examples/maui/bridge/Polymorfa.Example.Maui.Bridge.csproj @@ -0,0 +1 @@ +net8.0enableenabletrue diff --git a/packages/dotnet/examples/prepare-blazor.mjs b/packages/dotnet/examples/prepare-blazor.mjs new file mode 100644 index 00000000..01054eab --- /dev/null +++ b/packages/dotnet/examples/prepare-blazor.mjs @@ -0,0 +1,6 @@ +import { cpSync, mkdirSync } from "node:fs"; +const target = new URL("aspnet-core/wwwroot/blazor/", import.meta.url); +mkdirSync(target, { recursive: true }); +cpSync(new URL("blazor-wasm/bin/served/wwwroot/", import.meta.url), target, { + recursive: true, +}); diff --git a/packages/dotnet/src/Polymorfa.Sdk/AccountResources.cs b/packages/dotnet/src/Polymorfa.Sdk/AccountResources.cs new file mode 100644 index 00000000..cff70427 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/AccountResources.cs @@ -0,0 +1,85 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +public sealed record Organization(string Id, string ExternalId, string Name, string? Slug, string Email, string? Timezone, decimal CreditBalanceCents, decimal LowBalanceThresholdCents, string? BillingEmail, string? Status, string? Plan, string? PlanStatus, bool IsActive, long CreatedAt, long UpdatedAt); +public sealed record OrganizationMember([property: JsonPropertyName("_id")] string Id, [property: JsonPropertyName("_creationTime")] long CreationTime, string OrgId, string UserId, string Email, string? Name, string Role, string Status, long? InvitedAt, long? JoinedAt); +public sealed record ApiKey([property: JsonPropertyName("_id")] string RecordId, [property: JsonPropertyName("_creationTime")] long CreationTime, string Id, string KeyId, string Start, string Last4, string OrgId, string Label, long Scopes, string Source, long ExpiresAt, bool IsActive, long? LastUsed = null); +public sealed record ApiKeyDeactivation(bool Ok, string KeyId); +public sealed record ProjectToken(string Id, string Start, string Last4, string? Label, long Scopes, long? ExpiresAt, long CreatedAt, long? LastUsedAt, long? RevokedAt); +public sealed record AuditLog(string Id, string ActorEmail, string? ActorUserId, string? ActorRole, string Action, string Resource, string? ProjectId, string? ProjectName, string? Ip, string? UserAgent, long? Duration, string? Source, string? Description, string Result, JsonElement Metadata, long CreatedAt); +public sealed record ListAuditLogsParameters(string? Action = null, string? Resource = null, int? Limit = null); +public sealed record SessionBan(string Id, string SessionName, int? BanCode, string? BanReason, long? BanExpiresAt, long OccurredAt, string Status); +public sealed record SecurityIncident(string Id, string KeyId, string TokenType, string Source, string? Url, string? Ref, string Resolution, long DetectedAt, long? AcknowledgedAt, string? AcknowledgedBy, long CreatedAt); +public sealed record SecurityIncidentAcknowledgement(bool Acknowledged); +public sealed record MintClientTokenRequest(string EphemeralId, string? Session = null, string? Customer = null, IReadOnlyList? Allow = null, int? TtlSeconds = null); +public sealed record ClientTokenValue(string Token, string ExpiresAt) { public override string ToString() => $"ClientTokenValue(redacted, expiresAt={ExpiresAt})"; } +public sealed record ClientRules(string RecipientMode, string AllowedActions, int RateLimit, int MaxDaily, string AllowedOrigins, int ConversationTtlSeconds, int MaxConcurrency, int MaxSetupsPerMinute, string AllowedNumber, bool Enabled); +public sealed record SetClientRulesRequest(string RecipientMode, bool Enabled, string? AllowedActions = null, int? RateLimit = null, int? MaxDaily = null, string? AllowedOrigins = null, int? ConversationTtlSeconds = null, int? MaxConcurrency = null, int? MaxSetupsPerMinute = null, string? AllowedNumber = null); +public sealed record ResolveIdentityParameters(string? PhoneNumber = null, string? Id = null, string? Username = null, string? UsernameKey = null); +public sealed record ResolveIdentityResult(string? Id = null, string? Bsuid = null, string? PhoneNumber = null, string? Username = null, bool? KeyRequired = null); +public sealed record UserSecurityCode(string Id, string NumericCode, string QrCode, string? PhoneNumber = null, string? Username = null); + +public sealed class Organizations : Resource +{ + internal Organizations(HttpTransport http) : base(http) { } + public Task>> RetrieveAsync(RequestOptions? options = null) => Get>("/platform/team", options); +} +public sealed class Members : Resource +{ + internal Members(HttpTransport http) : base(http) { } + public Task>>> ListAsync(RequestOptions? options = null) => Get>>("/platform/members", options); +} +public sealed class ApiKeys : Resource +{ + internal ApiKeys(HttpTransport http) : base(http) { } + public Task>>> ListAsync(RequestOptions? options = null) => Get>>("/platform/keys", options); + public Task>> DeactivateAsync(string id, RequestOptions? options = null) => Delete>("/platform/keys/" + E(id), options); +} +public sealed class ProjectTokens : Resource +{ + internal ProjectTokens(HttpTransport http) : base(http) { } + public Task>>> ListAsync(string projectId, RequestOptions? options = null) => Get>>("/platform/tokens", options, new { projectId }); +} +public sealed class AuditLogs : Resource +{ + internal AuditLogs(HttpTransport http) : base(http) { } + public Task>>> ListAsync(ListAuditLogsParameters? parameters = null, RequestOptions? options = null) => Get>>("/platform/audit", options, parameters); +} +public sealed class SecurityIncidents : Resource +{ + internal SecurityIncidents(HttpTransport http) : base(http) { } + public Task>>> ListAsync(RequestOptions? options = null) => Get>>("/platform/incidents", options); + public Task>> AcknowledgeAsync(string id, RequestOptions? options = null) => Post>("/platform/incidents/" + E(id) + "/acknowledge", null, options); +} +public sealed class SessionBans : Resource +{ + internal SessionBans(HttpTransport http) : base(http) { } + public Task>>> ListAsync(RequestOptions? options = null) => Get>>("/platform/bans", options); + public Task>>> ListActiveAsync(RequestOptions? options = null) => Get>>("/platform/bans/active", options); +} +public sealed class ClientTokens : Resource +{ + internal ClientTokens(HttpTransport http) : base(http) { } + private static string Path(string session) => $"/platform/sessions/{E(session)}/client-rules"; + public Task>> MintAsync(MintClientTokenRequest body, RequestOptions? options = null) + { + Http.Credential.RequireServer(); + if (string.IsNullOrEmpty(body.Session) == string.IsNullOrEmpty(body.Customer)) throw new PolymorfaConfigurationException("Specify exactly one session or customer."); + return Post>("/platform/client-tokens", body, options); + } + public Task>> RetrieveRulesAsync(string session, RequestOptions? options = null) { Http.Credential.RequireServer(); return Get>(Path(session), options); } + public Task> UpdateRulesAsync(string session, SetClientRulesRequest body, RequestOptions? options = null) { Http.Credential.RequireServer(); return Put(Path(session), body, options); } + public Task> DeleteRulesAsync(string session, RequestOptions? options = null) { Http.Credential.RequireServer(); return Delete(Path(session), options); } +} +public sealed class Identities : Resource +{ + internal Identities(HttpTransport http) : base(http) { } + public Task>> ResolveAsync(string session, ResolveIdentityParameters parameters, RequestOptions? options = null) => Get>($"/messaging/{E(session)}/identities/resolve", options, parameters); +} +public sealed class Users : Resource +{ + internal Users(HttpTransport http) : base(http) { } + public Task>> GetSecurityCodeAsync(string session, string user, RequestOptions? options = null) => Get>($"/messaging/{E(session)}/users/{E(user)}/security-code", options); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Analytics.cs b/packages/dotnet/src/Polymorfa.Sdk/Analytics.cs new file mode 100644 index 00000000..71340fae --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Analytics.cs @@ -0,0 +1,113 @@ +namespace Polymorfa.Sdk; + +public record CallOutcomeMetrics +{ + public long Total { get; init; } + public long Answered { get; init; } + public long Missed { get; init; } + public long Declined { get; init; } + public long Failed { get; init; } + public long Ringing { get; init; } + public decimal? AnswerRate { get; init; } + public decimal TalkSeconds { get; init; } + public long TimedAnswered { get; init; } + public long TimedPickup { get; init; } + public decimal? AverageTalkSeconds { get; init; } + public decimal? MedianTalkSeconds { get; init; } + public decimal? P95TalkSeconds { get; init; } + public decimal? AveragePickupMs { get; init; } + public decimal? P95PickupMs { get; init; } + public long ShortAnswered { get; init; } + public long Video { get; init; } +} +public sealed record CallDirections(CallOutcomeMetrics Inbound, CallOutcomeMetrics Outbound); +public sealed record CallMediaQuality(long MeasuredCalls, decimal? AverageJitterMs, decimal? AverageRttMs, long? PacketsLost); +public sealed record CallAppQuality(long MeasuredCalls, decimal? AverageJitterMs, decimal? AverageRttMs, decimal? PacketLossRate, long? Reconnects); +public sealed record CallCountByCode(string Code, long Count); +public sealed record CallFollowUp(long EligibleMissed, long ReturnedWithin24h, decimal? Rate, decimal? AverageDelayMs, long PendingWindow, long UnknownContact); +public sealed record CallBusinessMetrics : CallOutcomeMetrics +{ + public CallDirections Directions { get; init; } = null!; + public CallMediaQuality MediaQuality { get; init; } = null!; + public CallAppQuality AppQuality { get; init; } = null!; + public IReadOnlyList EndReasons { get; init; } = []; + public IReadOnlyList AppErrors { get; init; } = []; + public IReadOnlyList Transports { get; init; } = []; + public long MultiParticipantCalls { get; init; } + public CallFollowUp FollowUp { get; init; } = null!; +} +public sealed record WhatsAppBusinessSegment(string Dimension, string Key, long SendAttempts, long Sent, long SendFailures, decimal? SendFailureRate, long CompletedConversations, long DeliveredConversations, long ReadConversations, long RepliedConversations, decimal? DeliveryRate, decimal? ReadRate, decimal? ReplyRate, decimal? AverageCustomerReplyMs, IReadOnlyList? ReadRateInterval95, IReadOnlyList? ReplyRateInterval95, decimal? ReadRateDifference, decimal? ReplyRateDifference, decimal? ShareOfConversations, decimal? ShareOfSends); +public sealed record WhatsAppEngagement(int WindowHours, long CompletedConversations, long DeliveredConversations, long ReadConversations, long RepliedConversations, decimal? DeliveryRate, decimal? ReadRate, decimal? ReplyRate, decimal? AverageCustomerReplyMs, bool Complete, long DroppedConversations, long DroppedRecords, long DroppedReceiptJoins); +public sealed record WhatsAppPlatformCount(string Platform, long Messages, decimal? Share); +public sealed record WhatsAppDeviceInventoryEntry(int DeviceIndex, string EstimatedPlatform, string ReportedClass, long? LastActiveAt, bool? Listed); +public sealed record WhatsAppDeviceInventory(bool ListObserved, bool ListCurrent, long? ObservedAt, int? DeviceCount, bool Truncated, IReadOnlyList Devices); +public sealed record WhatsAppDeviceAnalytics(string Detector, bool Measured, bool Complete, long ObservedBuckets, long? CustomerMessages, long? AccountMessages, IReadOnlyList CustomerPlatforms, IReadOnlyList AccountPlatforms, WhatsAppDeviceInventory? Inventory); +public sealed record WhatsAppRecipientActivityRow(long Ts, string RecipientCountry, string RecipientDeviceCount, string DeviceSource, long IncomingMessages, long DeliveryReceipts, long ReadReceipts, long OnlineSignals, long OfflineSignals, long TypingSignals, long LastSignalAt, long QuietGaps, long QuietGapMs, decimal? AverageQuietGapMs); +public sealed record WhatsAppRecipientActivity(bool Measured, bool Complete, long ObservedBuckets, long DroppedSignals, bool Truncated, IReadOnlyList Rows); +public sealed record WhatsAppConversationGroup(long Ts, string MessageType, string TextBand, string Origin, string CallingCode, string CustomerDevices, long CompletedConversations, long DeliveredConversations, long ReadConversations, long RepliedConversations, decimal ReplyLatencySumMs, decimal? ReadRate, decimal? ReplyRate, decimal? AverageCustomerReplyMs, string? RecipientCountry = null, string? RecipientDeviceCount = null); +public sealed record WhatsAppConversationBucket(long Ts, bool Complete); +public sealed record WhatsAppConversationBreakdown(bool Measured, bool Complete, long ObservedBuckets, long DroppedConversations, bool Truncated, IReadOnlyList Buckets, IReadOnlyList Rows); +public sealed record WhatsAppMessageAnalysis(bool Complete, IReadOnlyList Segments); +public sealed record WhatsAppCustomerActivity(bool Observed, long OnlineSignals, long OfflineSignals, long TypingSignals); +public sealed record WhatsAppAccountActivity(bool Observed, long PrimaryPhoneActivitySignals, long PrimaryPhoneActivePeriods, long CompletedPhoneActivityPeriods, long PhoneActivityMs, decimal? AveragePhoneActivityMs, long PhoneQuietGaps, long PhoneQuietMs, decimal? AveragePhoneQuietMs, long PrimaryPhoneMessages, long OtherDeviceMessages, long PrimaryPhoneReplies, long OtherDeviceReplies, decimal? AveragePrimaryPhoneResponseMs, decimal? AverageOtherDeviceResponseMs, long? LastPrimaryPhoneAt); +public sealed record WhatsAppResponseQueue(long AwaitingReply, long OldestWaitingMs, long ObservedAt, bool Complete); +public record WhatsAppMetrics +{ + public WhatsAppRecipientActivity? RecipientActivity { get; init; } + public WhatsAppDeviceAnalytics DeviceAnalytics { get; init; } = null!; + public WhatsAppConversationBreakdown ConversationBreakdown { get; init; } = null!; + public CallBusinessMetrics Calls { get; init; } = null!; + public bool Measured { get; init; } + public long ObservedHours { get; init; } + public long? LastObservedAt { get; init; } + public long OutgoingMessages { get; init; } + public long IncomingMessages { get; init; } + public long SendAttempts { get; init; } + public long SendFailures { get; init; } + public decimal? SendFailureRate { get; init; } + public long BusinessReplies { get; init; } + public decimal? AverageBusinessResponseMs { get; init; } + public long OnlineMs { get; init; } + public long Disconnects { get; init; } + public long ConnectFailures { get; init; } + public long StreamErrors { get; init; } + public long KeepaliveTimeouts { get; init; } + public WhatsAppEngagement Engagement { get; init; } = null!; + public WhatsAppMessageAnalysis MessageAnalysis { get; init; } = null!; + public WhatsAppCustomerActivity CustomerActivity { get; init; } = null!; + public WhatsAppAccountActivity AccountActivity { get; init; } = null!; + public WhatsAppResponseQueue? ResponseQueue { get; init; } +} +public sealed record WhatsAppAnalyticsSummary : WhatsAppMetrics { public long TotalNumbers { get; init; } public long MeasuredNumbers { get; init; } public long ConnectedNumbers { get; init; } } +public sealed record WhatsAppAnalyticsNumber : WhatsAppMetrics { public string SessionId { get; init; } = ""; public string ProjectId { get; init; } = ""; public string ProjectName { get; init; } = ""; public string Name { get; init; } = ""; public string Backend { get; init; } = ""; public string Status { get; init; } = ""; } +public sealed record WhatsAppAnalyticsCallSeries : CallOutcomeMetrics { public string SessionId { get; init; } = ""; public long Ts { get; init; } } +public sealed record WhatsAppAnalyticsSeries(long CustomerOnlineSignals, long CustomerTypingSignals, long PrimaryPhoneMessages, long OtherDeviceMessages, long PrimaryPhoneReplies, long PhoneActivePeriods, long CompletedPhoneActivityPeriods, long PhoneActivityMs, long PhoneQuietGaps, long PhoneQuietMs, string SessionId, long Ts, long OutgoingMessages, long IncomingMessages, long SendFailures, long BusinessReplies, decimal? AverageBusinessResponseMs, long OnlineMs, long Disconnects); +public sealed record AnalyticsPeriod(long Start, long End); +public sealed record AnalyticsRequestVitals(long Requests, long Failures, decimal? ErrorRate); +public sealed record WhatsAppAnalytics(IReadOnlyList CallSeries, bool Enabled, AnalyticsPeriod Period, AnalyticsRequestVitals RequestVitals, WhatsAppAnalyticsSummary? Summary, IReadOnlyList Numbers, IReadOnlyList Series); +public sealed record AnalyticsParameters(string? ProjectId = null, string? SessionId = null, long? Start = null, long? End = null); +public sealed record AnalyticsMetricsParameters(string? ProjectId = null, string? SessionId = null, int? WindowHours = null, bool? Segments = null, string? Format = null); +public sealed class Analytics : Resource +{ + private readonly string? projectId; + private string Path => projectId is null ? "/platform/analytics" : "/platform/projects/" + E(projectId) + "/analytics"; + internal Analytics(HttpTransport http, string? projectId) : base(http) => this.projectId = projectId; + private void Validate(string? project, string? session) + { + foreach (var id in new[] { project, session }) if (id is not null && !Guid.TryParseExact(id, "D", out _)) throw new PolymorfaValidationException("Analytics filters require UUIDs.", "invalid_analytics_filter"); + if (projectId is not null && project is not null && !projectId.Equals(project, StringComparison.OrdinalIgnoreCase)) throw new PolymorfaValidationException("Analytics cannot read outside the client's project.", "invalid_analytics_filter"); + } + public async Task> GetAsync(AnalyticsParameters? parameters = null, RequestOptions? options = null) + { + parameters ??= new(); Validate(parameters.ProjectId, parameters.SessionId); + if (parameters.Start is < 0 or > 9007199254740991 || parameters.End is < 0 or > 9007199254740991 || parameters.Start is { } start && parameters.End is { } end && (end < start || end - start > 366L * 86400000)) throw new PolymorfaValidationException("Choose an ordered analytics range of up to 366 days.", "invalid_analytics_range"); + var r = await ExistingContractCodec.Request>(Http, HttpMethod.Get, Path, options: options, query: Query(parameters with { ProjectId = projectId is null ? parameters.ProjectId : null })).ConfigureAwait(false); return new(r.Data.Data, r.Metadata); + } + public async Task> MetricsAsync(AnalyticsMetricsParameters? parameters = null, RequestOptions? options = null) + { + parameters ??= new(); Validate(parameters.ProjectId, parameters.SessionId); if (parameters.WindowHours is < 1 or > 168) throw new PolymorfaValidationException("windowHours must be from 1 to 168.", "invalid_analytics_range"); var format = parameters.Format ?? "prometheus"; if (format is not ("prometheus" or "openmetrics")) throw new PolymorfaValidationException("Invalid metrics format.", "invalid_analytics_filter"); + var accept = format == "openmetrics" ? "application/openmetrics-text" : "text/plain"; + var r = await Http.RequestTextAsync(Path + "/metrics", accept, options, Query(parameters with { ProjectId = projectId is null ? parameters.ProjectId : null, Format = format })).ConfigureAwait(false); + if (r.Metadata.Headers.GetValueOrDefault("content-type")?.Split(';')[0].Trim().ToLowerInvariant() != accept || !System.Text.RegularExpressions.Regex.IsMatch(r.Data, "^# TYPE polymorfa_analytics_enabled gauge$", System.Text.RegularExpressions.RegexOptions.Multiline) || format == "openmetrics" && !r.Data.EndsWith("# EOF\n", StringComparison.Ordinal)) throw new PolymorfaServerException("Invalid analytics metrics response.", "invalid_response", r.Metadata); return r; + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/AudienceProfiles.cs b/packages/dotnet/src/Polymorfa.Sdk/AudienceProfiles.cs new file mode 100644 index 00000000..dbcce335 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/AudienceProfiles.cs @@ -0,0 +1,36 @@ +namespace Polymorfa.Sdk; + +public sealed class AudienceProfiles : Resource +{ + public AudienceProfileImports Imports + { + get; + } + internal AudienceProfiles(HttpTransport http) : base(http) => Imports = new(http); + public Task>> ListAsync(ListAudienceProfilesParameters? parameters = null, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Get, "/platform/audience-profiles", options: CampaignContractOptions.Select(options), query: CampaignContractValidation.Page(parameters)); + + private static string Path(string phone) => "/platform/audience-profiles/" + Uri.EscapeDataString(CampaignContractValidation.Phone(phone)); + public Task>> RetrieveAsync(string phone, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Get, Path(phone), options: CampaignContractOptions.Select(options)); + public Task>> PutAsync(string phone, PutAudienceProfileRequest input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Put, Path(phone), CampaignContractValidation.Profile(input), CampaignContractOptions.Select(options, true)); + public Task>> DeleteAsync(string phone, long expectedRevision, RequestOptions? options = null) + { + CampaignContractValidation.Integer(expectedRevision, "expectedRevision", 1); + return CampaignContractCodec.Request>(Http, HttpMethod.Delete, Path(phone), options: CampaignContractOptions.Select(options, true), query: [new("expectedRevision", expectedRevision.ToString(System.Globalization.CultureInfo.InvariantCulture))]); + } + public Task>> ListConsentAsync(string phone, ListAudienceProfilesParameters? parameters = null, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Get, Path(phone) + "/consent", options: CampaignContractOptions.Select(options), query: CampaignContractValidation.Page(parameters)); +} +public sealed class AudienceProfileImports : Resource +{ + internal AudienceProfileImports(HttpTransport http) : base(http) { } + public Task>> CreateAsync(CreateAudienceProfileImportRequest input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Post, "/platform/audience-profile-imports", CampaignContractValidation.Import(input), CampaignContractOptions.Select(options, true)); + public Task>> RetrieveAsync(string jobId, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Get, "/platform/audience-profile-imports/" + CampaignContractValidation.Uuid(jobId, "jobId"), options: CampaignContractOptions.Select(options)); + public Task>> CancelAsync(string jobId, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Delete, "/platform/audience-profile-imports/" + CampaignContractValidation.Uuid(jobId, "jobId"), options: CampaignContractOptions.Select(options, true)); +} +internal static class CampaignContractOptions +{ + internal static RequestOptions Select(RequestOptions? options, bool write = false, bool replay = false) + { + var selected = (options ?? new()) with { ApiVersion = options?.ApiVersion ?? "2026-10-14", MaxNetworkRetries = write && !replay && options?.MaxNetworkRetries is null ? 0 : options?.MaxNetworkRetries }; + return replay ? selected.WithIdempotency() : selected; + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Audiences.cs b/packages/dotnet/src/Polymorfa.Sdk/Audiences.cs new file mode 100644 index 00000000..10ed08b1 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Audiences.cs @@ -0,0 +1,37 @@ +using System.Text.Json; + +namespace Polymorfa.Sdk; + +public sealed record CampaignRecipientInput(string Phone, IReadOnlyDictionary? Variables = null); +public sealed record InvalidRecipientRow(int Row, string Reason); +public sealed record AudienceImportMapping(string Phone, IReadOnlyDictionary? Variables = null); +public sealed record CreateAudienceRequest +{ + public string Name { get; } + public string? Source { get; } + public IReadOnlyList? Members { get; } + public string? FileId { get; } + public AudienceImportMapping? Mapping { get; } + private CreateAudienceRequest(string name, string? source, IReadOnlyList? members, string? fileId, AudienceImportMapping? mapping) { Name = name; Source = source; Members = members; FileId = fileId; Mapping = mapping; } + public static CreateAudienceRequest FromMembers(string name, IReadOnlyList? members = null, string? source = null) => new(name, source, members, null, null); + public static CreateAudienceRequest FromFile(string name, string fileId, AudienceImportMapping mapping, string? source = null) => new(name, source, null, fileId, mapping); +} +public sealed record AudienceImportResult(string Id, string Name, string Source, long RecipientCount, string? FileId, IReadOnlyList? Columns, IReadOnlyDictionary? SampleRow, IReadOnlyDictionary? Mapping, long CreatedAt, long UpdatedAt, long DuplicateCount, long InvalidCount, IReadOnlyList InvalidRows); +public sealed record AudienceMember(string Id, string Phone, IReadOnlyDictionary Variables, long CreatedAt); +public sealed record ListAudienceMembersParameters(string? Cursor = null, int? Limit = null); +public sealed record AddAudienceMembersRequest(IReadOnlyList Members); +public sealed record AddAudienceMembersResult(string ListId, long Added, long RecipientCount, long DuplicateCount, long InvalidCount, IReadOnlyList InvalidRows); +public sealed record DeleteAudienceMemberResult(bool Removed, string ListId, string Phone, long RecipientCount); +public sealed partial class Audiences : Resource +{ + internal Audiences(HttpTransport http) : base(http) { } + private static string Path(string id) => "/platform/audiences/" + E(id); + public Task>>> ListAsync(RequestOptions? options = null) => Get>>("/platform/audiences", options); + public Task>> CreateAsync(CreateAudienceRequest body, RequestOptions? options = null) => Post>("/platform/audiences", body, options); + public Task>>> RetrieveAsync(string id, RequestOptions? options = null) => Get>>(Path(id), options); + public Task>>> DeleteAsync(string id, RequestOptions? options = null) => Delete>>(Path(id), options); + public Task>>> CreateUploadAsync(IReadOnlyDictionary? body = null, RequestOptions? options = null) => Post>>("/platform/audiences/uploads", body, options); + public Task>> AddMembersAsync(string id, AddAudienceMembersRequest body, RequestOptions? options = null) => Post>(Path(id) + "/members", body, options is { MaxNetworkRetries: not null, IdempotencyKey: not null } ? options : (options ?? new()) with { MaxNetworkRetries = 0 }); + public Task> ListMembersAsync(string id, ListAudienceMembersParameters? parameters = null, RequestOptions? options = null) => CursorPage.LoadAsync(Http, Path(id) + "/members", Query(parameters), options ?? new()); + public Task>> DeleteMemberAsync(string id, string phone, RequestOptions? options = null) => Delete>(Path(id) + "/members/" + E(phone), options); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/BanSafe.cs b/packages/dotnet/src/Polymorfa.Sdk/BanSafe.cs new file mode 100644 index 00000000..6fde3136 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/BanSafe.cs @@ -0,0 +1,122 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +public sealed record BanSafeHealthProbabilities(decimal Healthy, decimal Limited, decimal Restricted, decimal Banned); +public sealed record BanSafeHealthPenalties(decimal Conduct, decimal Delivery, decimal Connection, decimal Restriction, decimal Total); +public sealed record BanSafeHealthFactor(string Group, string Key, decimal Penalty, decimal ObservedValue, long SampleSize); +public sealed record BanSafeHealthExplanation(BanSafeHealthPenalties Penalties, IReadOnlyList Factors, IReadOnlyList MeasuredGroups, IReadOnlyList MissingGroups); +public sealed record BanSafeObservedAccountState(string State, string ObservedAt, string Source); +public record BanSafeHealthProjection +{ + public decimal? Health { get; init; } + public string? Band { get; init; } + public string HealthSource { get; init; } = null!; + public string? HealthEstimatorVersion { get; init; } + public string? HealthModelVersion { get; init; } + public string? HealthEvaluatedAt { get; init; } + public decimal? HealthFeatureCoverage { get; init; } + public string HealthReliability { get; init; } = null!; + public string? HealthUnavailableReason { get; init; } + public BanSafeHealthProbabilities? HealthProbabilities { get; init; } + public string? MostLikelyHealthState { get; init; } + public BanSafeHealthExplanation? HealthExplanation { get; init; } + public BanSafeObservedAccountState? ObservedAccountState { get; init; } +} +public record BanSafeNumber : BanSafeHealthProjection +{ + public string SessionId { get; init; } = null!; + public string Session { get; init; } = null!; + public string PhoneNumber { get; init; } = null!; + public string ProjectId { get; init; } = null!; + public BanSafeNumberEnforcement? Enforcement { get; init; } +} +public sealed record BanSafeNumberEnforcement(string Rung, string PreviousRung, string OrganizationFloor, string Reason, string Source, decimal? ThroughputPerMinute, bool BlocksUnsolicited, bool Suspended, string StartedAt, string? EligibleLiftAt, decimal ExitProgress, IReadOnlyList BlockingFindings, bool OperatorHold, string AppealState, string State); +public sealed record BanSafeNumberWarmup(bool Enabled, string? TenureSource, int TenureDay, int? Allowance, int? SentToday, string? ResetsAt, IReadOnlyList Curve); +public sealed record BanSafeNumberDetail : BanSafeNumber +{ + public BanSafeNumberWarmup Warmup { get; init; } = null!; + public IReadOnlyList Findings { get; init; } = null!; + public string? LiftRequires { get; init; } + public string AppealState { get; init; } = null!; +} +public sealed record BanSafeHealthHistory(string SessionId, string Session, IReadOnlyList Points); +public sealed record BanSafeFinding(string? Id, string Key, string Title, string Summary, string Fix, string Status, string? Severity, long Occurrences, long ReopenedCount, IReadOnlyDictionary Evidence, string SessionId, string Session, string PhoneNumber, string? FirstSeenAt, string? LastSeenAt, string? AcknowledgedAt, string? AcknowledgedBy, string? AcknowledgementNote, string? SnoozedUntil, string? ResolvedAt, string? ResolveReason); +public sealed record BanSafeEnforcementSummary : BanSafeNumber +{ + public string Rung { get; init; } = null!; + public string PreviousRung { get; init; } = null!; + public string OrganizationFloor { get; init; } = null!; + public string Reason { get; init; } = null!; + public string Source { get; init; } = null!; + public decimal? ThroughputPerMinute { get; init; } + public bool BlocksUnsolicited { get; init; } + public bool Suspended { get; init; } + public IReadOnlyList BlockingFindings { get; init; } = null!; + public string StartedAt { get; init; } = null!; + public string? EligibleLiftAt { get; init; } + public string LiftRequires { get; init; } = null!; + public bool OperatorHold { get; init; } + public string AppealState { get; init; } = null!; + public string State { get; init; } = null!; +} +public sealed record BanSafeIncident(string Id, string SessionId, string Session, string PhoneNumber, string ProjectId, string Kind, string Source, string Resolution, bool Ambiguous, string StartedAt, string? EndsAt, string? ClosedAt, string? ClosedBy, string? ClaimId, string? Note, string? ReportedBy, string CreatedAt); +public sealed record ReportBanSafeIncidentRequest(string Session, string? OccurredAt = null, string? Note = null); +public sealed record BanSafeIncidentReceipt(string IncidentId, bool Created, string SessionId, string Session, string OccurredAt); +public sealed record BanSafeClaimEvidence(long? AttributionRuleVersion, int WindowDays, bool DeviceEvidence, long OtherDevices, bool RestrictedInWindow, long CriticalFindingDays, bool SharedConnection, decimal MeasuredHours, bool SimultaneousCalls); +public sealed record BanSafeClaim(string Id, string IncidentId, string SessionId, string Session, string PhoneNumber, string ProjectId, string Status, string Verdict, string WindowStart, string WindowEnd, decimal MeasuredCents, decimal CapCents, decimal AmountCents, BanSafeClaimEvidence Evidence, string Summary, string Reason, string? DecidedAt, string? PaidAt, string CreatedAt); +public sealed record BanSafeSignalDefinition(string Key, string Label, string Group, string Kind, string Unit, string Description); +public sealed record BanSafeCodeCount(long Code, long Count); +[JsonConverter(typeof(BanSafeSignalValueConverter))] +public abstract record BanSafeSignalValue; +public sealed record BanSafeNumberSignal(decimal Value) : BanSafeSignalValue; +public sealed record BanSafeBooleanSignal(bool Value) : BanSafeSignalValue; +public sealed record BanSafeEnumSignal(string Value) : BanSafeSignalValue; +public sealed record BanSafeHistogramSignal(IReadOnlyList Values) : BanSafeSignalValue; +public sealed record BanSafeSignal(string Key, string Label, string Group, string Kind, string Unit, string Description, bool Measured, BanSafeSignalValue? Value, long? SampleSize, IReadOnlyList? Codes); +internal sealed class BanSafeSignalValueConverter : JsonConverter +{ + public override BanSafeSignalValue Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); var value = document.RootElement; + return value.ValueKind switch { JsonValueKind.Number => new BanSafeNumberSignal(value.GetDecimal()), JsonValueKind.True => new BanSafeBooleanSignal(true), JsonValueKind.False => new BanSafeBooleanSignal(false), JsonValueKind.String => new BanSafeEnumSignal(value.GetString()!), JsonValueKind.Array => new BanSafeHistogramSignal(value.EnumerateArray().Select(v => v.GetDecimal()).ToArray()), _ => throw new JsonException("Invalid BanSafe signal value.") }; + } + public override void Write(Utf8JsonWriter writer, BanSafeSignalValue value, JsonSerializerOptions options) + { + switch (value) { case BanSafeNumberSignal n: writer.WriteNumberValue(n.Value); break; case BanSafeBooleanSignal b: writer.WriteBooleanValue(b.Value); break; case BanSafeEnumSignal e: writer.WriteStringValue(e.Value); break; case BanSafeHistogramSignal h: JsonSerializer.Serialize(writer, h.Values, options); break; default: throw new JsonException("Invalid BanSafe signal value."); } + } +} +public sealed record BanSafeCollectionStatus(string State, string? LatestFlushedAt, string? LatestReceivedAt, string? FreshUntil, long? RecordVersion, long? CollectorVersion, bool? Partial, long? DroppedRecords); +public sealed record BanSafeTelemetrySnapshot(string BucketStart, string FlushedAt, string ReceivedAt, bool Partial, long? RecordVersion, IReadOnlyList Signals); +public sealed record BanSafeCollectionSession(string SessionId, string Session, string ProjectId, BanSafeCollectionStatus Collection); +public sealed record BanSafeTelemetryDetail(string SessionId, string Session, string ProjectId, BanSafeCollectionStatus Collection, BanSafeTelemetrySnapshot? Snapshot); +public sealed record BanSafeHealthAction(string Id, string SessionId, string Session, string ProjectId, string Mode, string Action, string Status, decimal Health, decimal Threshold, string HealthSource, string EstimatorVersion, string? ModelVersion, decimal? SlowDownMps, string EvaluatedAt, string CreatedAt, string? CompletedAt, string? Outcome); +public sealed record ListBanSafeHealthParameters(string? ProjectId = null, string? Cursor = null, int? Limit = null); +public sealed record ListBanSafeHistoryParameters(string? Since = null, int? Limit = null); +public sealed record ListBanSafeFindingsParameters(string? ProjectId = null, string? Cursor = null, int? Limit = null, string? Session = null, string? Status = null, string? Severity = null); +public sealed record ListBanSafeEnforcementParameters(string? ProjectId = null, string? Cursor = null, int? Limit = null, string? Rung = null); +public sealed record ListBanSafeIncidentsParameters(string? ProjectId = null, string? Cursor = null, int? Limit = null, string? Session = null); +public sealed record ListBanSafeClaimsParameters(string? ProjectId = null, string? Cursor = null, int? Limit = null, string? Session = null, string? Status = null); +public sealed record ListBanSafeTelemetryHistoryParameters(string? Since = null, string? Until = null, string? Cursor = null, int? Limit = null); +public sealed record ListBanSafeHealthActionsParameters(string? ProjectId = null, string? Cursor = null, int? Limit = null, string? Session = null, string? Status = null); +public sealed class BanSafe : Resource +{ + internal BanSafe(HttpTransport http) : base(http) { } + public Task> ListHealthAsync(ListBanSafeHealthParameters? parameters = null, RequestOptions? options = null) => Page("health", parameters, options); + public Task>> GetHealthAsync(string session, RequestOptions? options = null) => Get>("/platform/bansafe/health/" + E(session), options); + public Task>> ListHealthHistoryAsync(string session, ListBanSafeHistoryParameters? parameters = null, RequestOptions? options = null) => Get>("/platform/bansafe/health/" + E(session) + "/history", options, parameters); + public Task>>> ListSignalsAsync(RequestOptions? options = null) => Get>>("/platform/bansafe/signals", options); + public Task>> GetTelemetryAsync(string session, RequestOptions? options = null) => Get>("/platform/bansafe/telemetry/" + E(session), options); + public Task> ListTelemetryHistoryAsync(string session, ListBanSafeTelemetryHistoryParameters? parameters = null, RequestOptions? options = null) => Page("telemetry/" + E(session) + "/history", parameters, options); + public Task> ListCollectionAsync(ListBanSafeHealthParameters? parameters = null, RequestOptions? options = null) => Page("collection", parameters, options); + public Task> ListHealthActionsAsync(ListBanSafeHealthActionsParameters? parameters = null, RequestOptions? options = null) => Page("health-actions", parameters, options); + public Task> ListFindingsAsync(ListBanSafeFindingsParameters? parameters = null, RequestOptions? options = null) => Page("findings", parameters, options); + public Task> ListEnforcementAsync(ListBanSafeEnforcementParameters? parameters = null, RequestOptions? options = null) => Page("enforcement", parameters, options); + public Task> ListIncidentsAsync(ListBanSafeIncidentsParameters? parameters = null, RequestOptions? options = null) => Page("incidents", parameters, options); + public Task>> CreateIncidentAsync(ReportBanSafeIncidentRequest body, RequestOptions options) => Post>("/platform/bansafe/incidents", body, options); + public Task>> RetractIncidentAsync(string id, RequestOptions? options = null) => Post>("/platform/bansafe/incidents/" + E(id) + "/retract", null, options); + public Task> ListClaimsAsync(ListBanSafeClaimsParameters? parameters = null, RequestOptions? options = null) => Page("claims", parameters, options); + public Task>> GetClaimAsync(string id, RequestOptions? options = null) => Get>("/platform/bansafe/claims/" + E(id), options); + private Task> Page(string suffix, object? parameters, RequestOptions? options) => CursorPage.LoadAsync(Http, "/platform/bansafe/" + suffix, Query(parameters), options ?? new()); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Billing.cs b/packages/dotnet/src/Polymorfa.Sdk/Billing.cs new file mode 100644 index 00000000..d5dc073b --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Billing.cs @@ -0,0 +1,69 @@ +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +public sealed record BillingBalance(decimal BalanceCents, string PreferredCurrency); +public sealed record BillingUsage(long ActiveNumbers, decimal TotalChargedCents); +public sealed record BillingTransaction(string Id, decimal AmountCents, decimal BalanceAfterCents, string Type, string Description, string? SessionId, string? ProjectId, string? Tier, string? Currency, string PaymentStatus, long CreatedAt); +public sealed record TierPricing(string Id, string Tier, decimal DailyRateCents, string Label, string Description, IReadOnlyList Features); +public sealed record BillingPriority(string Id, string Name, int Priority); +public sealed record ProjectResourcePriority(string Id, string Name, int Priority, string ProjectId); +public sealed record BillingPriorities(long Revision, IReadOnlyList Projects, IReadOnlyList Customers, IReadOnlyList Numbers); +public sealed record BillingLimit(string Scope, string ResourceId, string ProjectId, string Name, decimal? LimitCredits, decimal SpentCredits, decimal ReservedCredits, long Revision); +public sealed record DailyCredits(string Date, decimal Credits); +public sealed record BillingLimits(string CheckedAt, string PeriodStart, string PeriodEnd, decimal TodayCredits, decimal MonthCredits, IReadOnlyList Daily, IReadOnlyList Budgets); +public sealed record ResourceBillingControls(BillingLimit Budget, int Priority, long PriorityRevision); +public sealed record BillingReadParameters(string? ProjectId = null, string? Scope = null); +public sealed record SetBillingLimitRequest([property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] decimal? LimitCredits, long ExpectedRevision); +public sealed record SetBillingPriorityRequest(int Priority, long ExpectedRevision); +public sealed record SetResourceBillingControlsRequest([property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] decimal? LimitCredits, int Priority, long ExpectedBudgetRevision, long ExpectedPriorityRevision); +public sealed record SavedResult(bool Saved); +public sealed record BillingResourceOrder(string Scope, string ResourceId); +public sealed record ReorderBillingPrioritiesRequest(string Scope, long ExpectedRevision, string? ProjectId = null, IReadOnlyList? ResourceIds = null, IReadOnlyList? Resources = null); +public sealed class Billing : Resource +{ + internal Billing(HttpTransport http) : base(http) { } + public Task>> RetrieveAsync(RequestOptions? options = null) => Get>("/platform/billing", options); + public Task>> UsageAsync(RequestOptions? options = null) => Get>("/platform/billing/usage", options); + public Task>>> ListTransactionsAsync(RequestOptions? options = null) => Get>>("/platform/billing/transactions", options); + public Task>>> ListPricingAsync(RequestOptions? options = null) => Get>>("/platform/billing/pricing", options); + public Task>> GetResourceControlsAsync(string scope, string resourceId, RequestOptions? options = null) => Get>(Path("controls", scope, resourceId), options); + public Task>> SetResourceControlsAsync(string scope, string resourceId, SetResourceBillingControlsRequest body, RequestOptions? options = null) + { + Limit(body.LimitCredits); Priority(body.Priority); Revision(body.ExpectedBudgetRevision); Revision(body.ExpectedPriorityRevision); + return Put>(Path("controls", scope, resourceId), body, options); + } + public Task>> GetLimitsAsync(BillingReadParameters? parameters = null, RequestOptions? options = null) => Get>("/platform/billing/limits", options, Read(parameters)); + public Task>> SetLimitAsync(string scope, string resourceId, SetBillingLimitRequest body, RequestOptions? options = null) { Limit(body.LimitCredits); Revision(body.ExpectedRevision); return Put>(Path("limits", scope, resourceId), body, options); } + public Task>> GetPrioritiesAsync(BillingReadParameters? parameters = null, RequestOptions? options = null) => Get>("/platform/billing/priorities", options, Read(parameters)); + public Task>> SetPriorityAsync(string scope, string resourceId, SetBillingPriorityRequest body, RequestOptions? options = null) { Priority(body.Priority); Revision(body.ExpectedRevision); return Put>(Path("priorities", scope, resourceId), body, options); } + public Task>> ReorderPrioritiesAsync(ReorderBillingPrioritiesRequest body, RequestOptions? options = null) + { + Revision(body.ExpectedRevision); + if (body.Scope == "resource") + { + var resources = body.Resources ?? throw Invalid("resources is required."); + if (body.ResourceIds is not null || resources.Count > 1000000) throw Invalid("Invalid resource ordering."); + var normalized = resources.Select(row => row.Scope is "customer" or "number" ? new BillingResourceOrder(row.Scope, Id(row.ResourceId)) : throw Invalid("Resource scope must be customer or number.")).ToArray(); + if (normalized.Distinct().Count() != normalized.Length) throw Invalid("Resources must be unique."); + return Put>("/platform/billing/priorities", body with { ProjectId = Id(body.ProjectId!), Resources = normalized }, options); + } + Scope(body.Scope); + var ids = (body.ResourceIds ?? throw Invalid("resourceIds is required.")).Select(Id).ToArray(); + if (body.Resources is not null || ids.Distinct().Count() != ids.Length) throw Invalid("Resource IDs must be unique."); + return Put>("/platform/billing/priorities", body with { ResourceIds = ids, ProjectId = body.Scope == "project" ? null : Id(body.ProjectId!) }, options); + } + private static string Path(string group, string scope, string id) { Scope(scope); return $"/platform/billing/{group}/{scope}/{Id(id)}"; } + private static BillingReadParameters? Read(BillingReadParameters? parameters) + { + if (parameters?.Scope is not null && parameters.Scope != "project") throw Invalid("Read scope must be project."); + return parameters?.ProjectId is null ? parameters : parameters with { ProjectId = Id(parameters.ProjectId) }; + } + internal static string Id(string value) { if (value is null || !Regex.IsMatch(value, "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")) throw Invalid("Resource ID must be a UUID."); return value.ToLowerInvariant(); } + internal static void Scope(string value) { if (value is not ("project" or "customer" or "number")) throw Invalid("Scope must be project, customer, or number."); } + internal static void Revision(long value) { if (value is < 0 or > 2147483646) throw Invalid("Invalid expected revision."); } + internal static void Priority(int value) { if (value is < 0 or > 1000000) throw Invalid("Priority must be between 0 and 1000000."); } + internal static void Limit(decimal? value) { if (value is < 0 or > 1000000 || value is decimal number && decimal.Round(number, 6) != number) throw Invalid("Credit limits require at most six decimal places and must be between 0 and 1000000."); } + private static PolymorfaValidationException Invalid(string message) => new(message, "invalid_billing_control"); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Business.cs b/packages/dotnet/src/Polymorfa.Sdk/Business.cs new file mode 100644 index 00000000..d1b3a874 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Business.cs @@ -0,0 +1,40 @@ +namespace Polymorfa.Sdk; + +public sealed class Business : Resource +{ + internal Business(HttpTransport http) : base(http) { } + private static string Path(string session) => $"/messaging/{E(session)}/business"; + private static string Profile(string session) => Path(session) + "/profile"; + private static string Product(string session, string product) => Path(session) + "/products/" + E(product); + private static string Collection(string session, string collection) => Path(session) + "/collections/" + E(collection); + public Task>> GetProfileAsync(string session, RequestOptions? options = null) => Get>(Profile(session), options); + public Task>>> UpdateProfileAsync(string session, BusinessProfileUpdateRequest body, RequestOptions? options = null) => Patch>>(Profile(session), body, options); + public Task>>> SetCoverPhotoAsync(string session, PictureRequest body, RequestOptions? options = null) { ValidateImage(body.Url, body.Base64); return Put>>(Profile(session) + "/cover-photo", body, options); } + public Task>>> DeleteCoverPhotoAsync(string session, string coverPhotoId, RequestOptions? options = null) => Delete>>(Profile(session) + "/cover-photo/" + E(coverPhotoId), options); + public Task>> GetCatalogAsync(string session, BusinessCatalogParameters parameters, RequestOptions? options = null) => Get>(Path(session) + "/catalog", options, parameters); + public Task>>> CreateCatalogAsync(string session, RequestOptions? options = null) => Post>>(Path(session) + "/catalog", null, options); + public Task>>> SetCartEnabledAsync(string session, bool enabled, RequestOptions? options = null) => Patch>>(Path(session) + "/catalog/cart", new { enabled }, options); + public Task>> GetProductAsync(string session, string productId, string businessId, RequestOptions? options = null) => Get>(Product(session, productId), options, new { id = businessId }); + public Task>>> CreateProductAsync(string session, BusinessProductMutationRequest body, RequestOptions? options = null) { ValidateProduct(body); return Post>>(Path(session) + "/products", body, options); } + public Task>>> UpdateProductAsync(string session, string productId, BusinessProductMutationRequest body, RequestOptions? options = null) { ValidateProduct(body); return Put>>(Product(session, productId), body, options); } + public Task>>> DeleteProductAsync(string session, string productId, RequestOptions? options = null) => Delete>>(Product(session, productId), options); + public Task>>> SetProductVisibilityAsync(string session, string productId, bool hidden, RequestOptions? options = null) => Patch>>(Product(session, productId) + "/visibility", new { hidden }, options); + public Task>>> AppealProductAsync(string session, string productId, BusinessCatalogAppealRequest body, RequestOptions? options = null) => Post>>(Product(session, productId) + "/appeal", body, options); + public Task>> ListCollectionsAsync(string session, BusinessCollectionsParameters parameters, RequestOptions? options = null) => Get>(Path(session) + "/collections", options, parameters); + public Task>> GetCollectionAsync(string session, string collectionId, BusinessCatalogParameters parameters, RequestOptions? options = null) => Get>(Collection(session, collectionId), options, parameters); + public Task>>> CreateCollectionAsync(string session, BusinessCollectionCreateRequest body, RequestOptions? options = null) => Post>>(Path(session) + "/collections", body, options); + public Task>>> UpdateCollectionAsync(string session, string collectionId, BusinessCollectionUpdateRequest body, RequestOptions? options = null) => Patch>>(Collection(session, collectionId), body, options); + public Task>>> DeleteCollectionAsync(string session, string collectionId, RequestOptions? options = null) => Delete>>(Collection(session, collectionId), options); + public Task>>> ReorderCollectionsAsync(string session, BusinessCollectionReorderRequest body, RequestOptions? options = null) => Post>>(Path(session) + "/collections/reorder", body, options); + public Task>>> AppealCollectionAsync(string session, string collectionId, BusinessCatalogAppealRequest body, RequestOptions? options = null) => Post>>(Collection(session, collectionId) + "/appeal", body, options); + public Task>>> GetOrderAsync(string session, string orderId, BusinessOrderLookupRequest body, RequestOptions? options = null) => Post>>(Path(session) + "/orders/" + E(orderId) + "/lookup", body, options); + public Task>> GetMerchantComplianceAsync(string session, RequestOptions? options = null) => Get>(Path(session) + "/compliance", options); + public Task>>> SetMerchantComplianceAsync(string session, BusinessMerchantCompliance body, RequestOptions? options = null) => Put>>(Path(session) + "/compliance", body, options); + public Task>> GetLinkedAccountsAsync(string session, RequestOptions? options = null) => Get>(Path(session) + "/linked-accounts", options); + public Task>> GetEligibilityAsync(string session, RequestOptions? options = null) => Get>(Path(session) + "/eligibility", options); + private static void ValidateProduct(BusinessProductMutationRequest body) + { + foreach (var image in body.Images) if (new[] { image.Url, image.Base64, image.MediaUrl }.Count(v => v is not null) != 1) throw new PolymorfaValidationException("Product images require exactly one source.", "invalid_parameter"); + } + private static void ValidateImage(string? url, string? base64) { if ((url is null) == (base64 is null)) throw new PolymorfaValidationException("Cover photo requires exactly one source.", "invalid_parameter"); } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/BusinessModels.cs b/packages/dotnet/src/Polymorfa.Sdk/BusinessModels.cs new file mode 100644 index 00000000..389eedba --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/BusinessModels.cs @@ -0,0 +1,59 @@ +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +public sealed record BusinessProfileDay(string DayOfWeek, string Mode, int? OpenTime = null, int? CloseTime = null); +public sealed record BusinessProfileHoursUpdate(string TimeZone, IReadOnlyList Days); +public sealed record BusinessProfileUpdateRequest(string? Address = null, string? Email = null, string? Description = null, IReadOnlyList? Websites = null, BusinessProfileHoursUpdate? Hours = null); +public sealed record BusinessProfileStatus(string Status); +public sealed record BusinessCoverPhotoResult(string CoverPhotoId); +public sealed record BusinessActionSuccess(bool Success); +public sealed record BusinessCatalogParameters(string Id, string? After = null, int? Limit = null, int? Width = null, int? Height = null); +public sealed record BusinessCollectionsParameters(string Id, string? After = null, int? CollectionLimit = null, int? ItemLimit = null, int? Width = null, int? Height = null); +public sealed record BusinessProductImageSource(string? Url = null, string? Base64 = null, string? MediaUrl = null); +public sealed record BusinessImporterAddress(string? Street1 = null, string? Street2 = null, string? City = null, string? Region = null, string? PostalCode = null, string? CountryCode = null); +public sealed record BusinessProductCompliance(string? CountryCodeOrigin = null, string? ImporterName = null, BusinessImporterAddress? ImporterAddress = null); +public sealed record BusinessProductMutationRequest(string Name, IReadOnlyList Images, string? Description = null, string? Currency = null, string? Price = null, string? SalePrice = null, string? Url = null, string? RetailerId = null, bool? Hidden = null, IReadOnlyList? VideoUrls = null, string? ComplianceCategory = null, BusinessProductCompliance? Compliance = null, int? Width = null, int? Height = null); +public sealed record BusinessProductImage(string Id, string? OriginalUrl = null, string? RequestUrl = null); +public sealed record BusinessProductVideo(string Id, string? OriginalUrl = null, string? ThumbnailUrl = null); +public sealed record BusinessProductMedia(IReadOnlyList Images, IReadOnlyList Videos); +public sealed record BusinessSalePrice(string Price, string? StartDate = null, string? EndDate = null); +public sealed record BusinessProductStatus(bool CanAppeal, string? Status = null); +public sealed record BusinessDimensions(int? Width = null, int? Height = null); +public sealed record BusinessVariantThumbnail(BusinessDimensions OriginalDimensions, string? Id = null, string? OriginalUrl = null, string? RequestUrl = null); +public sealed record BusinessVariantProperty(string Name, string Value); +public sealed record BusinessVariantAvailabilityItem(bool Available, IReadOnlyList Options, string? ProductId = null); +public sealed record BusinessVariantAvailability(IReadOnlyList Listings); +public sealed record BusinessVariantListing(string? Description = null, string? LowestPrice = null, string? MultiPrice = null); +public sealed record BusinessVariantOption(string Value, BusinessVariantThumbnail? Thumbnail = null); +public sealed record BusinessVariantType(string Name, IReadOnlyList Options); +public sealed record BusinessProductVariant(BusinessVariantAvailability Availability, BusinessVariantListing ListingDetails, IReadOnlyList Types, IReadOnlyList Properties); +public sealed record BusinessProduct(string Id, string Name, string Price, string Currency, bool Hidden, bool Sanctioned, BusinessProductMedia Media, BusinessProductStatus Status, string? RetailerId = null, string? BelongsTo = null, string? Description = null, string? Url = null, string? ShimmedUrl = null, decimal? MaxAvailable = null, string? Availability = null, string? ComplianceCategory = null, BusinessProductCompliance? Compliance = null, BusinessSalePrice? SalePrice = null, BusinessProductVariant? Variant = null); +public sealed record BusinessCatalogPage(IReadOnlyList Products, string? Next = null, string? Previous = null); +public sealed record BusinessProductDeleteResult(int DeletedCount); +public sealed record BusinessCollectionStatus(bool CanAppeal, string? Status = null, string? CommerceUrl = null, string? RejectReason = null); +public sealed record BusinessCollection(string Id, string Name, IReadOnlyList Products, BusinessCollectionStatus Status); +public sealed record BusinessCollectionPage(IReadOnlyList Collections, string? Next = null); +public sealed record BusinessCollectionCreateRequest(string Name, IReadOnlyList ProductIds); +public sealed record BusinessCollectionUpdateRequest(string? Name = null, IReadOnlyList? AddProductIds = null, IReadOnlyList? RemoveProductIds = null); +public sealed record BusinessCollectionMutationResult(string Id, string ReviewStatus); +public sealed record BusinessCollectionMove(string CollectionId, int FromIndex, int ToIndex); +public sealed record BusinessCollectionReorderRequest(IReadOnlyList Moves); +public sealed record BusinessCatalogAppealRequest(string Reason); +public sealed record BusinessOrderLookupRequest(string Token) +{ + public override string ToString() => "BusinessOrderLookupRequest { Token = [REDACTED] }"; +} +public sealed record BusinessOrderPrice(string Subtotal, string Total, string Currency, string? PriceStatus = null); +public sealed record BusinessOrderProduct(string Id, string Price, string Currency, string Name, decimal Quantity, string? ImageId = null, string? ImageUrl = null, string? VariantProperties = null); +public sealed record BusinessOrder(string Id, long CreatedAt, BusinessOrderPrice Price, IReadOnlyList Products, string? CatalogId = null); +public sealed record BusinessMerchantContact(string Email, string LandlineNumber, string MobileNumber); +public sealed record BusinessMerchantOfficer(string Name, string Email, string LandlineNumber, string MobileNumber); +public sealed record BusinessMerchantCompliance(string EntityName, string EntityType, bool IsRegistered, string EntityTypeCustom, BusinessMerchantContact CustomerCare, BusinessMerchantOfficer GrievanceOfficer); +public sealed record BusinessFacebookPage(string Id, string DisplayName, string ProfilePictureUrl, bool ShowOnProfile, bool WhatsAppAsPageButton, [property: JsonPropertyName("hasActiveCTWAAd")] bool HasActiveCtwaAd, bool HasCreatedAd, string? ProfileSync = null); +public sealed record BusinessFacebookBusiness(string Id, string DisplayName, string? CatalogId = null, string? CatalogState = null); +public sealed record BusinessInstagramProfessional(string Handle, string DisplayName, string ProfilePictureUrl, bool ShowOnProfile); +public sealed record BusinessWhatsAppAdIdentity(string Id, [property: JsonPropertyName("hasActiveCTWAAd")] bool HasActiveCtwaAd, bool HasCreatedAd); +public sealed record BusinessLinkedAccounts(BusinessFacebookPage? FacebookPage = null, BusinessFacebookBusiness? FacebookBusiness = null, BusinessInstagramProfessional? InstagramProfessional = null, BusinessWhatsAppAdIdentity? WhatsAppAdIdentity = null); +public sealed record BusinessFeatureEligibility(string Feature, string Status, long? Expiration = null, string? AdditionalParams = null, bool? ShowPrivacyInterstitialToNewUsers = null, bool? V1Enabled = null); +public sealed record BusinessEligibility(IReadOnlyList Features); diff --git a/packages/dotnet/src/Polymorfa.Sdk/Call.cs b/packages/dotnet/src/Polymorfa.Sdk/Call.cs new file mode 100644 index 00000000..49b86a38 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Call.cs @@ -0,0 +1,182 @@ +using System.Text.Json; +using System.Threading.Channels; + +namespace Polymorfa.Sdk; + +public sealed class Call +{ + private readonly CallsClient owner; + private readonly object gate = new(); + private readonly CancellationTokenSource lifetime = new(); + private readonly Channel mediaFrames = System.Threading.Channels.Channel.CreateBounded(new BoundedChannelOptions(256) { FullMode = BoundedChannelFullMode.Wait }); + private readonly Dictionary roster = []; + private readonly Dictionary sources = []; + private readonly Dictionary rosterRevisions = []; + private readonly Queue errors = new(); + private readonly SemaphoreSlim diagnostics = new(1, 1); + private long rosterRevision; + private CallsSocket? media; + private Task? mediaRunner; + private Task? answer; + private Task? acceptRequest; + private volatile string state; + private bool accepted; + private bool claimedByUs; + private int reconnects; + private DateTimeOffset? lastQuality; + private bool reportsStopped; + private int reportsQueued; + public string Id { get; } + public string Session => owner.Options.Session; + public string Direction { get; } + public string Peer { get; } + private readonly bool offeredVideo; + public bool HasVideo => offeredVideo && Capabilities.Video; + public CallCapabilities Capabilities { get; internal set; } = new(); + public string ConnectionId { get; } = CallsProtocol.CreateConnectionId(); + public string State => state; + public bool Ended => state == "ended"; + public string? EndReason { get; private set; } + public bool Answered { get; private set; } + public string? AnsweredBy { get; private set; } + public bool Exclusive { get; private set; } + public bool ClaimedByOther { get; private set; } + public bool CanJoin => state == "incoming" && Answered && !Exclusive && !ClaimedByOther; + public bool? RemoteAudioMuted { get; private set; } + public int? SampleRate { get; private set; } + public bool HandRaised { get; private set; } + public bool SocialSupported { get; private set; } + public CallMediaState MediaState { get; private set; } = new(false, false); + public IReadOnlyList Participants { get { lock (gate) return roster.Values.ToArray(); } } + public IReadOnlyList VideoSources { get { lock (gate) return sources.Values.ToArray(); } } + internal Call(CallsClient owner, string id, string direction, string peer, bool video, bool exclusive) { this.owner = owner; Id = id; Direction = direction; Peer = peer; offeredVideo = video; claimedByUs = exclusive; Exclusive = exclusive; accepted = direction == "outbound"; state = direction == "inbound" ? "incoming" : "ringing"; } + public IAsyncEnumerable MediaFramesAsync(CancellationToken cancellationToken = default) => mediaFrames.Reader.ReadAllAsync(cancellationToken); + public Task AnswerAsync(bool exclusive = false, bool? video = null, CancellationToken cancellationToken = default) + { + lock (gate) { if (answer is not null) return answer; if (state != "incoming" || ClaimedByOther) throw new CallsSocketException("This call cannot be answered by this participant.", ClaimedByOther ? "call_claimed" : "invalid_state"); answer = AnswerCoreAsync(exclusive, video ?? HasVideo, cancellationToken); return answer; } + } + public Task JoinAsync(bool? video = null, CancellationToken cancellationToken = default) { if (!Answered && state == "incoming") throw new CallsSocketException("Nobody has answered this call yet.", "call_not_answered"); return AnswerAsync(false, video, cancellationToken); } + private async Task AnswerCoreAsync(bool exclusive, bool video, CancellationToken cancellation) + { + await Task.Yield(); var changed = false; + try + { + Transition("connecting"); acceptRequest = owner.RequestAsync(async (c, participant, options) => (await c.Calls.AcceptAsync(Id, new(exclusive, video, participant), options).ConfigureAwait(false)).Data.Data, new() { CancellationToken = cancellation }); + var result = await acceptRequest.ConfigureAwait(false); accepted = changed = true; claimedByUs = result.Exclusive; Answered = result.Answered; AnsweredBy = result.AnsweredBy; Exclusive = result.Exclusive; MediaState = MediaState with { VideoEnabled = video }; + if (Ended) { await ReleaseAsync().ConfigureAwait(false); return; } + await AttachAsync(false, cancellation).ConfigureAwait(false); + } + catch (Exception error) + { + if (changed) { ReportFailure(error); await ReleaseAsync().ConfigureAwait(false); Finish(error is CallsSocketException { CloseCode: 4409 } ? "claimed" : "connection_failed"); } + else if (!Ended) { if (error is PolymorfaException { Code: "call_claimed" }) ClaimedByOther = true; Transition("incoming"); } + throw; + } + finally { lock (gate) answer = null; } + } + internal void Apply(CallLifecycleEvent e) + { + if (Ended) return; + if (e.Event == "call.accepted") + { + Capabilities = CallsClient.Capabilities(e.Payload, Capabilities); Answered = true; AnsweredBy = CallsClient.Field(e.Payload, "answeredBy"); Exclusive = CallsClient.Flag(e.Payload, "exclusive") || claimedByUs; + if (Direction == "outbound" && state == "ringing") { Transition("connecting"); Observe(AttachOutboundAsync()); } + else if (Exclusive && !accepted && answer is null && AnsweredBy != owner.SelfParticipant) ClaimedByOther = true; + owner.Publish(new CallChanged(this)); + } + else if (e.Event is "call.ended" or "call.missed" or "call.rejected") Finish(e.Event == "call.ended" ? CallsClient.Field(e.Payload, "reason") ?? "unknown" : e.Event[5..]); + else if (e.Event is "call.participant_joined" or "call.participant_state" && e.Payload.ValueKind == JsonValueKind.Object && e.Payload.TryGetProperty("participant", out var p)) { try { var value = p.Deserialize(HttpTransport.Json); if (value is not null) UpdateParticipant(value); } catch (JsonException) { } } + else if (e.Event == "call.participant_left" && CallsClient.Field(e.Payload, "participantId") is { } id) ParticipantLeft(id); + } + private async Task AttachOutboundAsync() { try { await AttachAsync(false, lifetime.Token).ConfigureAwait(false); } catch (Exception error) { if (!Ended) { ReportFailure(error); await ReleaseAsync().ConfigureAwait(false); Finish("connection_failed"); } } } + private async Task AttachAsync(bool refresh, CancellationToken cancellation) + { + using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellation, lifetime.Token); var connection = await CallsSocket.OpenMediaAsync(owner.Tokens, Id, ConnectionId, owner.SocketOptions, refresh, linked.Token).ConfigureAwait(false); + try + { + if (Ended) { await connection.DisposeAsync().ConfigureAwait(false); return; } + RemoteAudioMuted = null; + if (reconnects > 0 && MediaState != new CallMediaState(false, false)) MediaState = await connection.SetMediaStateAsync(MediaState.AudioMuted, MediaState.VideoEnabled, MediaState.ScreenSharing, linked.Token).ConfigureAwait(false); + if (Ended) { await connection.DisposeAsync().ConfigureAwait(false); return; } + SampleRate = connection.Ready.SampleRate; media = connection; Transition("connected"); mediaRunner = ReadMediaAsync(connection); + } + catch { await connection.DisposeAsync().ConfigureAwait(false); throw; } + } + private async Task ReadMediaAsync(CallsSocket connection) + { + Exception? failure = null; + try + { + await foreach (var frame in connection.FramesAsync(lifetime.Token).ConfigureAwait(false)) + { + if (frame is CallRemoteMedia mute) RemoteAudioMuted = mute.AudioMuted; + else if (frame is CallHandState hand) { HandRaised = hand.Raised; SocialSupported = hand.Supported; } + else if (frame is CallParticipantFrame participant) UpdateParticipant(participant.Participant); + else if (frame is CallParticipantLeft left) ParticipantLeft(left.ParticipantId); + else if (frame is CallVideoSource source) { lock (gate) sources[source.Source] = source; } + else if (frame is CallVideoSourceRemoved removed) { lock (gate) sources.Remove(removed.Source); } + else if (frame is CallSocketError error && error.Code is "call_claimed" or "calls_disabled") { failure = new CallsSocketException("Media was refused by the platform.", error.Code, error.Code == "call_claimed" ? 4409 : 4403); } + await mediaFrames.Writer.WriteAsync(frame, lifetime.Token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { } + catch (Exception error) { failure ??= error; } + finally { await connection.DisposeAsync().ConfigureAwait(false); } + lock (gate) sources.Clear(); + if (Ended || lifetime.IsCancellationRequested) return; + failure ??= new CallsSocketException("Media connection ended.", "socket_closed", connection.CloseCode); + if (failure is CallsSocketException { CloseCode: 1000 or 4409 }) { Finish(failure is CallsSocketException { Code: "call_claimed" } ? "claimed" : "remote_hangup"); return; } + if (failure is CallsSocketException { CloseCode: 4400 or 4403 or 1008 }) { ReportFailure(failure); await ReleaseAsync().ConfigureAwait(false); Finish("connection_failed"); return; } + Transition("reconnecting"); var refresh = failure is CallsSocketException { CloseCode: 4401 }; if (refresh) owner.Tokens.Invalidate(); + for (var attempt = 0; attempt < owner.Options.ReconnectAttempts && !Ended; attempt++) + { + try { await Task.Delay(TimeSpan.FromMilliseconds(Math.Min(owner.Options.MaxBackoff.TotalMilliseconds, owner.Options.MinBackoff.TotalMilliseconds * Math.Pow(2, attempt))), lifetime.Token).ConfigureAwait(false); reconnects++; await AttachAsync(refresh, lifetime.Token).ConfigureAwait(false); return; } + catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { return; } + catch (Exception error) { failure = error; if (error is CallsSocketException { CloseCode: 4400 or 4403 or 4409 or 1008 }) break; refresh |= error is CallsSocketException { CloseCode: 4401 }; } + } + if (!Ended) { ReportError("reconnect_exhausted"); await ReleaseAsync().ConfigureAwait(false); Finish("connection_failed"); } + } + public Task SendAudioAsync(short[] pcm, CancellationToken cancellationToken = default) => ConnectedMedia().SendAudioAsync(pcm, cancellationToken); + public Task SendVideoAsync(CallVideoFrame frame, CancellationToken cancellationToken = default) => ConnectedMedia().SendVideoAsync(frame, cancellationToken); + public async Task SetMediaStateAsync(bool? audioMuted = null, bool? videoEnabled = null, bool? screenSharing = null, CancellationToken cancellationToken = default) { var result = await ConnectedMedia().SetMediaStateAsync(audioMuted, videoEnabled, screenSharing, cancellationToken).ConfigureAwait(false); if (!Ended) MediaState = result; return result; } + private CallsSocket ConnectedMedia() => state == "connected" && media is { Connected: true } connection ? connection : throw new CallsSocketException("Media is not connected.", "invalid_state"); + public async Task RejectAsync(CancellationToken cancellationToken = default) { if (state != "incoming" || Answered) throw new CallsSocketException("Only a ringing unanswered call can be rejected.", "invalid_state"); await owner.RequestAsync(async (c, participant, o) => await c.Calls.RejectAsync(Id, new(participant), o).ConfigureAwait(false), new() { CancellationToken = cancellationToken }).ConfigureAwait(false); Finish("rejected"); } + public async Task EndAsync(CancellationToken cancellationToken = default) { if (Ended) return; await owner.RequestAsync(async (c, _, o) => await c.Calls.EndAsync(Id, o with { IdempotencyKey = "voip-end:" + Id }).ConfigureAwait(false), new() { CancellationToken = cancellationToken }).ConfigureAwait(false); Finish("hangup"); } + public async Task LeaveAsync(CancellationToken cancellationToken = default) + { + if (Ended) return; if (Direction == "outbound" && state == "ringing") { await EndAsync(cancellationToken).ConfigureAwait(false); return; } + if (acceptRequest is { } pending) { try { await pending.ConfigureAwait(false); } catch (PolymorfaException) { } } + if (Ended) return; + if (accepted) { if (media is { Connected: true } connection) await connection.LeaveAsync(cancellationToken).ConfigureAwait(false); else await owner.RequestAsync(async (c, participant, o) => await c.Calls.LeaveAsync(Id, new(ConnectionId, participant), o).ConfigureAwait(false), new() { CancellationToken = cancellationToken }).ConfigureAwait(false); } + Finish("left"); + } + public async Task AddParticipantAsync(string to, CancellationToken cancellationToken = default) + { + if (Ended || !Capabilities.Invite) throw new CallsSocketException("Participant invitations are unavailable.", "invalid_state"); long revision; lock (gate) revision = rosterRevision; + var value = await owner.RequestAsync(async (c, _, o) => (await c.Calls.AddParticipantAsync(Id, new(to), o).ConfigureAwait(false)).Data.Data, new() { CancellationToken = cancellationToken }).ConfigureAwait(false); lock (gate) { if (rosterRevisions.GetValueOrDefault(value.Id) <= revision) UpdateParticipant(value); } + return value; + } + public async Task RingParticipantAsync(string to, CancellationToken cancellationToken = default) { if (Ended || !Capabilities.Invite) throw new CallsSocketException("Participant ringing is unavailable.", "invalid_state"); await owner.RequestAsync(async (c, _, o) => await c.Calls.RingParticipantAsync(Id, new(to), o).ConfigureAwait(false), new() { CancellationToken = cancellationToken }).ConfigureAwait(false); } + public async Task SendReactionAsync(string emoji, CancellationToken cancellationToken = default) { if (state != "connected" || !SocialSupported) throw new CallsSocketException("Call reactions are unavailable.", "invalid_state"); await owner.RequestAsync(async (c, participant, o) => await c.Calls.SendReactionAsync(Id, new(ConnectionId, emoji, participant), o).ConfigureAwait(false), new() { CancellationToken = cancellationToken }).ConfigureAwait(false); } + public async Task SetHandRaisedAsync(bool raised, CancellationToken cancellationToken = default) { if (state != "connected" || !SocialSupported) throw new CallsSocketException("Call hand controls are unavailable.", "invalid_state"); await owner.RequestAsync(async (c, participant, o) => await c.Calls.SetHandRaisedAsync(Id, new(ConnectionId, raised, participant), o).ConfigureAwait(false), new() { CancellationToken = cancellationToken }).ConfigureAwait(false); } + private void UpdateParticipant(CallParticipant participant) { if (string.IsNullOrEmpty(participant.Id) || participant.State is not ("invited" or "ringing" or "connected" or "left")) return; lock (gate) { rosterRevisions[participant.Id] = ++rosterRevision; if (roster.TryGetValue(participant.Id, out var old) && Rank(old.State) > Rank(participant.State)) return; roster[participant.Id] = participant; } owner.Publish(new CallChanged(this)); } + private void ParticipantLeft(string id) { lock (gate) { rosterRevisions[id] = ++rosterRevision; if (roster.TryGetValue(id, out var old)) roster[id] = old with { State = "left" }; } owner.Publish(new CallChanged(this)); } + private static int Rank(string value) => value switch { "left" => 3, "connected" => 2, "ringing" => 1, _ => 0 }; + private void Transition(string next) { lock (gate) { if (Ended) return; state = next; } owner.Publish(new CallChanged(this)); } + private void Finish(string reason) { lock (gate) { if (Ended) return; state = "ended"; EndReason = reason; sources.Clear(); lifetime.Cancel(); } ReportQuality(new(Reconnects: reconnects)); mediaFrames.Writer.TryComplete(); if (media is { } connection) Observe(connection.DisposeAsync().AsTask()); owner.Completed(this, reason); } + internal async Task DisconnectAsync() { if (!Ended && accepted) { using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { await LeaveAsync(deadline.Token).ConfigureAwait(false); } catch (Exception e) when (e is PolymorfaException or OperationCanceledException) { } } Finish("left"); if (media is { } connection) await connection.DisposeAsync().ConfigureAwait(false); if (mediaRunner is not null) await mediaRunner.ConfigureAwait(false); } + private async Task ReleaseAsync() + { + if (!accepted || ClaimedByOther) return; using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { if (claimedByUs) await owner.RequestAsync(async (c, _, o) => await c.Calls.EndAsync(Id, o with { IdempotencyKey = "voip-end:" + Id }).ConfigureAwait(false), new() { CancellationToken = deadline.Token }).ConfigureAwait(false); else await owner.RequestAsync(async (c, participant, o) => await c.Calls.LeaveAsync(Id, new(ConnectionId, participant), o).ConfigureAwait(false), new() { CancellationToken = deadline.Token }).ConfigureAwait(false); } catch (PolymorfaException) { } catch (OperationCanceledException) { } + } + private void ReportFailure(Exception e) { ReportError(e is CallsSocketException { Code: "media_timeout" } ? "media_timeout" : e is CallsSocketException { CloseCode: 4401 } ? "token_refresh_failed" : "other"); owner.Publish(new CallsFailure(e)); } + public void ReportQuality(CallQuality quality) { lock (gate) { if (lastQuality is { } previous && DateTimeOffset.UtcNow - previous < TimeSpan.FromSeconds(5)) return; lastQuality = DateTimeOffset.UtcNow; } Report(new("quality", ConnectionId, new("polymorfa-dotnet", SdkVersion.Version, "other"), Quality: quality)); } + public void ReportError(string code) { lock (gate) { var now = DateTimeOffset.UtcNow; while (errors.TryPeek(out var old) && now - old >= TimeSpan.FromMinutes(1)) errors.Dequeue(); if (errors.Count >= 20) return; errors.Enqueue(now); } Report(new("error", ConnectionId, new("polymorfa-dotnet", SdkVersion.Version, "other"), Error: new(code))); } + private void Report(CallReportRequest report) { if (!owner.Options.Diagnostics || reportsStopped) return; if (Interlocked.Increment(ref reportsQueued) > 32) { Interlocked.Decrement(ref reportsQueued); return; } Observe(SendReportAsync(report)); } + private async Task SendReportAsync(CallReportRequest report) + { + await diagnostics.WaitAsync().ConfigureAwait(false); try { if (reportsStopped) return; using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await owner.RequestAsync(async (c, participant, o) => await c.Calls.ReportAsync(Id, report with { Participant = participant }, o).ConfigureAwait(false), new() { MaxNetworkRetries = 0, CancellationToken = timeout.Token }).ConfigureAwait(false); } catch (PolymorfaException error) { if (error.Status is >= 400 and < 500 and not 429) reportsStopped = true; } catch (OperationCanceledException) { } finally { diagnostics.Release(); Interlocked.Decrement(ref reportsQueued); } + } + private void Observe(Task task) { _ = ObserveAsync(task); } + private async Task ObserveAsync(Task task) { try { await task.ConfigureAwait(false); } catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { } catch (Exception error) { owner.Publish(new CallsFailure(error)); } } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CallConsent.cs b/packages/dotnet/src/Polymorfa.Sdk/CallConsent.cs new file mode 100644 index 00000000..027d4caa --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CallConsent.cs @@ -0,0 +1,63 @@ +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +public sealed record CallRetention(string Policy, int RetentionDays, IReadOnlyList AppliesTo, long Revision, string? UpdatedAt); +public sealed record UpdateCallRetentionRequest(string Policy, int? RetentionDays = null, long? ExpectedRevision = null); +public sealed class CallRetentionResource : Resource +{ + internal CallRetentionResource(HttpTransport http) : base(http) { } + public async Task> RetrieveAsync(RequestOptions? options = null) { var r = await Get>("/platform/call-retention", options); return new(r.Data.Data, r.Metadata); } + public async Task> UpdateAsync(UpdateCallRetentionRequest body, RequestOptions? options = null) { var r = await Put>("/platform/call-retention", body, options); return new(r.Data.Data, r.Metadata); } +} +public sealed class TeamCallRetentionClient : IDisposable +{ + private readonly HttpTransport http; + public CallRetentionResource CallRetention { get; } + public TeamCallRetentionClient(Credential credential, ClientOptions? options = null) { credential.RequireServer(); http = new(credential, options ?? new()); CallRetention = new(http); } + public void Dispose() => http.Dispose(); +} +public sealed record CallPolicy(IReadOnlyList BlockedCountryCodes, long OptOutCount, long Revision, string? UpdatedAt); +public sealed record UpdateCallPolicyRequest(IReadOnlyList BlockedCountryCodes, long? ExpectedRevision = null); +public sealed record CallOptOut(string Id, string? PhoneNumber, string? Bsuid, string? Note, string Source, string CreatedAt); +public sealed record ListCallOptOutsParameters(int? Limit = null, string? Cursor = null, string? PhoneNumber = null, string? Bsuid = null); +public sealed record CreateCallOptOutRequest(string? PhoneNumber = null, string? Bsuid = null, string? Note = null); +public sealed record CallOptOutImportEntry(string? PhoneNumber = null, string? Bsuid = null, string? Note = null); +public sealed record ImportCallOptOutsRequest(IReadOnlyList Entries); +public sealed record CallOptOutRejection(int Index, string Reason); +public sealed record CallOptOutImportResult(int Added, int Existing, IReadOnlyList Rejected); +public sealed record CallOptOutDeleted(string Id, bool Deleted); +public sealed class CallPolicyResource : Resource +{ + internal CallPolicyResource(HttpTransport http) : base(http) { } + public async Task> RetrieveAsync(RequestOptions? options = null) { var r = await Get>("/platform/call-policy", options); return new(r.Data.Data, r.Metadata); } + public async Task> UpdateAsync(UpdateCallPolicyRequest body, RequestOptions? options = null) + { + if (body.BlockedCountryCodes.Count > 300 || body.BlockedCountryCodes.Any(code => !Regex.IsMatch(code, "^[1-9][0-9]{0,3}$")) || body.ExpectedRevision is < 0 or > 9_007_199_254_740_991) throw new PolymorfaValidationException("Invalid blocked country codes or revision.", "invalid_parameter"); + var r = await Put>("/platform/call-policy", body, options); return new(r.Data.Data, r.Metadata); + } +} +public sealed class CallOptOuts : Resource +{ + internal CallOptOuts(HttpTransport http) : base(http) { } + public Task> ListAsync(ListCallOptOutsParameters? parameters = null, RequestOptions? options = null) + { + if (parameters?.PhoneNumber is not null && parameters.Bsuid is not null) throw new PolymorfaValidationException("Use phoneNumber or bsuid as the filter.", "invalid_parameter"); + return CursorPage.LoadAsync(Http, "/platform/call-opt-outs", Query(parameters), options ?? new()); + } + public async Task> CreateAsync(CreateCallOptOutRequest body, RequestOptions? options = null) + { + if (string.IsNullOrEmpty(body.PhoneNumber) == string.IsNullOrEmpty(body.Bsuid)) throw new PolymorfaValidationException("Supply exactly one phoneNumber or bsuid.", "invalid_parameter"); + var r = await Post>("/platform/call-opt-outs", body, options); return new(r.Data.Data, r.Metadata); + } + public async Task> ImportAsync(ImportCallOptOutsRequest body, RequestOptions? options = null) + { + if (body.Entries.Count is < 1 or > 5000) throw new PolymorfaValidationException("Import between 1 and 5000 entries.", "invalid_parameter"); + var r = await Post>("/platform/call-opt-outs/import", body, options); return new(r.Data.Data, r.Metadata); + } + public async Task> DeleteAsync(string id, RequestOptions? options = null) + { + if (string.IsNullOrWhiteSpace(id)) throw new PolymorfaConfigurationException("An opt-out ID is required."); + var r = await Delete>("/platform/call-opt-outs/" + E(id), options); return new(r.Data.Data, r.Metadata); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CallRecords.cs b/packages/dotnet/src/Polymorfa.Sdk/CallRecords.cs new file mode 100644 index 00000000..3dbb6e56 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CallRecords.cs @@ -0,0 +1,70 @@ +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +public sealed record CallRecord(string CallId, string? ProjectId, string SessionId, string Direction, string Upstream, string Outcome, string State, bool HasVideo, string? PeerRef, string StartedAt, string? ConnectedAt, string? EndedAt, decimal? DurationSeconds, string? EndReason); +public sealed record CallRecordEndReason(string Code, string Label); +public sealed record CallRecordParticipant(string Id, string State, string FirstSeenAt, string UpdatedAt, string? LeftReason); +public sealed record CallRecordConnection(string Id, string Participant, string Transport, string? JoinedAt, string? LeftAt, string? Reason); +public sealed record CallRecordTelemetry(string Status, string Source, decimal? SetupMs, decimal? RingMs, string? Codec, decimal? JitterMs, long? PacketsLost, decimal? RttMs, decimal? ReceivedKbps, decimal? SentKbps); +public sealed record CallRecordAppQuality(string ReportedAt, decimal? RttMs, decimal? JitterMs, long? PacketsLost, long? PacketsReceived, string? AudioCodec, string? VideoCodec, string? CandidateType, long? Reconnects); +public sealed record CallRecordAppError(string Code, string ReportedAt); +public sealed record CallRecordAppConnection(string ConnectionId, string Participant, CallReportClient? Client, CallRecordAppQuality? Quality, IReadOnlyList Errors); +public sealed record CallRecordAppReports(string Status, IReadOnlyList Connections, bool Truncated); +public sealed record CallRecordSummary(string CallId, string SessionId, string? ProjectId, string Direction, string State, bool Live, string Backend, bool HasVideo, string? PeerRef, string StartedAt, string? ConnectedAt, string? EndedAt, decimal? DurationSeconds, CallRecordEndReason? EndReason, string? AnsweredBy, bool? Exclusive); +public sealed record CallRecordHistoryEvent(string EventId, string Type, string OccurredAt); +public sealed record CallRecordHistory(IReadOnlyList Events, bool Truncated); +public sealed record CallRecordCorrelation(string CallId, string SessionId); +public sealed record CallRecordDetail(CallRecordSummary Call, IReadOnlyList Participants, IReadOnlyList Connections, CallRecordTelemetry Telemetry, CallRecordAppReports AppReports, CallRecordHistory History, CallRecordCorrelation Correlation); +public record CallStatsMetrics(long Calls, long Answered, long Missed, long Declined, long Failed, long InProgress, decimal? AnswerRate, decimal TotalDurationSeconds, decimal? AverageDurationSeconds); +public sealed record CallStatsGroup(long Calls, long Answered, long Missed, long Declined, long Failed, long InProgress, decimal? AnswerRate, decimal TotalDurationSeconds, decimal? AverageDurationSeconds, string Key, string? Start) : CallStatsMetrics(Calls, Answered, Missed, Declined, Failed, InProgress, AnswerRate, TotalDurationSeconds, AverageDurationSeconds); +public sealed record CallStatsHeatmapCell(int DayOfWeek, int Hour, long Calls, long Answered); +public sealed record CallStats(string Since, string Until, string Timezone, string GroupBy, CallStatsMetrics Totals, IReadOnlyList Groups, bool GroupsTruncated, IReadOnlyList Heatmap); +public record CallFilters(string? ProjectId = null, string? SessionId = null, string? Direction = null, string? Upstream = null, string? Outcome = null, string? Since = null, string? Until = null); +public sealed record CallStatsParameters(string? ProjectId = null, string? SessionId = null, string? Direction = null, string? Upstream = null, string? Outcome = null, string? Since = null, string? Until = null, string? GroupBy = null, string? Timezone = null) : CallFilters(ProjectId, SessionId, Direction, Upstream, Outcome, Since, Until); +public sealed record ListCallRecordsParameters(string? ProjectId = null, string? SessionId = null, string? Direction = null, string? Upstream = null, string? Outcome = null, string? Since = null, string? Until = null, int? Limit = null, string? Cursor = null) : CallFilters(ProjectId, SessionId, Direction, Upstream, Outcome, Since, Until); +public sealed record ExportCallRecordsParameters(string? ProjectId = null, string? SessionId = null, string? Direction = null, string? Upstream = null, string? Outcome = null, string? Since = null, string? Until = null, string? Format = null, int? Limit = null, string? Cursor = null) : CallFilters(ProjectId, SessionId, Direction, Upstream, Outcome, Since, Until); +public sealed record CallRecordExportPage(string Format, string Body, string? NextCursor); +public sealed class CallRecords : Resource +{ + private readonly string? projectId; + internal CallRecords(HttpTransport http, string? projectId) : base(http) => this.projectId = projectId; + private Dictionary Filters(CallFilters parameters) + { + var result = new Dictionary(); + if (projectId is not null && parameters.ProjectId is not null && projectId != parameters.ProjectId) throw new PolymorfaConfigurationException("A project client reads only its own project's calls."); + if ((projectId ?? parameters.ProjectId) is { } project) result["projectId"] = Text(project, 64); + if (parameters.SessionId is { } session) result["sessionId"] = Text(session, 128); + if (parameters.Direction is { } direction) result["direction"] = OneOf(direction, "inbound", "outbound"); + if (parameters.Upstream is { } upstream) result["upstream"] = OneOf(upstream, "linked_device", "cloud_api"); + if (parameters.Outcome is { } outcome) result["outcome"] = OneOf(outcome, "answered", "missed", "declined", "failed", "in_progress"); + foreach (var (key, value) in new[] { ("since", parameters.Since), ("until", parameters.Until) }) if (value is not null) { if (!Regex.IsMatch(value, "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$") || !DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out _)) throw new PolymorfaConfigurationException("Call timestamps require a real ISO date-time with a time zone."); result[key] = value; } + return result; + } + private static string Text(string value, int maximum) => !string.IsNullOrWhiteSpace(value) && value.Length <= maximum ? value : throw new PolymorfaConfigurationException("Invalid call filter."); + private static string OneOf(string value, params string[] values) => values.Contains(value) ? value : throw new PolymorfaConfigurationException("Invalid call filter."); + private static void Limit(Dictionary query, int? limit, string? cursor, int max) { if (limit is { } n) { if (n < 1 || n > max) throw new PolymorfaConfigurationException("Invalid call page limit."); query["limit"] = n.ToString(CultureInfo.InvariantCulture); } if (cursor is not null) query["cursor"] = Text(cursor, 256); } + public async Task> RetrieveAsync(string id, string? projectId = null, RequestOptions? options = null) + { + if (!Regex.IsMatch(id, "^[\\x21-\\x7e]{1,128}$")) throw new PolymorfaConfigurationException("Invalid callId."); var r = await Get>("/platform/calls/" + E(id), options, Filters(new(ProjectId: projectId))).ConfigureAwait(false); return new(r.Data.Data, r.Metadata); + } + public async Task> StatsAsync(CallStatsParameters? parameters = null, RequestOptions? options = null) + { + parameters ??= new(); var query = Filters(parameters); if (parameters.GroupBy is { } group) query["groupBy"] = OneOf(group, "day", "hour", "session", "outcome"); if (parameters.Timezone is { } zone) query["timezone"] = Text(zone, 64); var r = await Get>("/platform/calls/stats", options, query).ConfigureAwait(false); return new(r.Data.Data, r.Metadata); + } + public Task> ListAsync(ListCallRecordsParameters? parameters = null, RequestOptions? options = null) { parameters ??= new(); var query = Filters(parameters); Limit(query, parameters.Limit, parameters.Cursor, 100); return CursorPage.LoadAsync(Http, "/platform/calls", Query(query), options ?? new()); } + public async Task> ExportAsync(ExportCallRecordsParameters? parameters = null, RequestOptions? options = null) + { + parameters ??= new(); var query = Filters(parameters); var format = OneOf(parameters.Format ?? "csv", "csv", "ndjson"); query["format"] = format; Limit(query, parameters.Limit, parameters.Cursor, 1000); var contentType = format == "csv" ? "text/csv" : "application/x-ndjson"; + var r = await Http.RequestTextAsync("/platform/calls/export", contentType, options, Query(query)).ConfigureAwait(false); + if (r.Metadata.Headers.GetValueOrDefault("content-type")?.Split(';')[0].Trim().ToLowerInvariant() != contentType) throw new PolymorfaServerException("Unexpected call export content type.", "invalid_response", r.Metadata); + var next = r.Metadata.Headers.GetValueOrDefault("polymorfa-next-cursor"); return new(new(format, r.Data, string.IsNullOrEmpty(next) ? null : next), r.Metadata); + } + public async IAsyncEnumerable ExportAllAsync(ExportCallRecordsParameters? parameters = null, RequestOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + parameters ??= new(); options ??= new(); using var linked = CancellationTokenSource.CreateLinkedTokenSource(options.CancellationToken, cancellationToken); options = options with { CancellationToken = linked.Token }; var seen = new HashSet(); if (parameters.Cursor is { } initial) seen.Add(initial); var first = true; + while (true) { var r = await ExportAsync(parameters, options).ConfigureAwait(false); if (r.Data.NextCursor is { } next && !seen.Add(next)) throw new PolymorfaServerException("Repeated call export cursor.", "invalid_response", r.Metadata); var body = r.Data.Body; if (!first && r.Data.Format == "csv") { var end = body.IndexOf('\n'); body = end < 0 ? "" : body[(end + 1)..]; } first = false; if (body.Length > 0) yield return body; if (r.Data.NextCursor is null) yield break; parameters = parameters with { Cursor = r.Data.NextCursor }; } + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Calls.cs b/packages/dotnet/src/Polymorfa.Sdk/Calls.cs new file mode 100644 index 00000000..7934269f --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Calls.cs @@ -0,0 +1,101 @@ +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +public sealed record PlaceCallRequest(string? Session = null, string? To = null, IReadOnlyList? Participants = null, string? GroupId = null, bool? Video = null, bool? Exclusive = null, string? Participant = null); +public sealed record PlacedCall(string CallId, string Session, bool Video); +public sealed record AcceptCallRequest(bool? Exclusive = null, bool? Video = null, string? Participant = null); +public sealed record AcceptedCall(bool Answered, string AnsweredBy, bool Exclusive); +public sealed record RejectIncomingCallRequest(string From); +public sealed record RejectIncomingCallResponse(bool Success, string? Message, System.Text.Json.JsonElement? Data); +public sealed record RejectCallRequest(string? Participant = null); +public sealed record LeaveCallRequest(string ConnectionId, string? Participant = null); +public sealed record AddCallParticipantRequest(string To); +public sealed record CallParticipant(string Id, bool AudioMuted, bool Video, string State, string? PhoneNumber = null, string? Bsuid = null, string? Username = null, bool? HandRaised = null); +public sealed record CreateCallLinkRequest(string Session, bool? Video = null); +public sealed record PreviewCallLinkRequest(string Session, string Token, bool? Video = null); +public sealed record CreatedCallLink(string Session, string Token, string Url, bool Video); +public sealed record PreviewedCallLink(string Session, bool Video, ConversationReference CreatorConversationIdentity, bool ApprovalRequired, bool IsAdmin); +public sealed record CallReactionRequest(string ConnectionId, string Emoji, string? Participant = null); +public sealed record CallHandRequest(string ConnectionId, bool Raised, string? Participant = null); +public sealed record CheckCallRequest(string Session, string To); +public sealed record CallPermissionLimit(string Period, long MaxAllowed, long Used, string? ResetsAt); +public sealed record CallPermissionAction(bool Allowed, IReadOnlyList Limits); +public sealed record CallPermissionActions(CallPermissionAction? RequestPermission, CallPermissionAction? StartCall); +public record CallPermissionState(string Status, string? ExpiresAt, string? Source, string? UpdatedAt, string? CheckedAt, bool Fresh, CallPermissionActions? Actions); +public sealed record CallPermission(string Status, string? ExpiresAt, string? Source, string? UpdatedAt, string? CheckedAt, bool Fresh, CallPermissionActions? Actions, ConversationReference Conversation) : CallPermissionState(Status, ExpiresAt, Source, UpdatedAt, CheckedAt, Fresh, Actions); +public sealed record CallCheck(bool Allowed, string? Refusal, CallPermissionState? Permission); +public sealed record SessionCallSettings(bool CallsEnabled, bool ConferenceMode, string InboundRoute, string? SipTrunkId, bool SipClaim, bool HostCloudApiCalls, long Revision, string? UpdatedAt, bool SimultaneousCalls); +public sealed record UpdateCallSettingsRequest(bool? CallsEnabled = null, bool? ConferenceMode = null, string? InboundRoute = null, [property: System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] PatchValue SipTrunkId = default, bool? SipClaim = null, bool? HostCloudApiCalls = null, long? ExpectedRevision = null, bool? SimultaneousCalls = null, bool? AcknowledgeRisk = null); +public sealed record CallReportClient(string Sdk, string Version, string Platform); +public sealed record CallQuality(int? RttMs = null, int? JitterMs = null, int? PacketsLost = null, int? PacketsReceived = null, int? Reconnects = null, string? AudioCodec = null, string? VideoCodec = null, string? CandidateType = null); +public sealed record CallReportError(string Code); +public sealed record CallReportRequest(string Kind, string ConnectionId, CallReportClient Client, string? Participant = null, CallQuality? Quality = null, CallReportError? Error = null); + +public sealed class Calls +{ + private readonly HttpTransport http; + internal Calls(HttpTransport http) => this.http = http; + private static string Path(string id) => $"/messaging/voip/calls/{Uri.EscapeDataString(id)}"; + private static void NonEmpty(string? value, string name) { if (string.IsNullOrWhiteSpace(value)) throw new PolymorfaValidationException($"{name} is required.", "invalid_parameter"); } + private void Participant(string? value) + { + if (value is null) return; + if (http.Credential.Kind == CredentialKind.ClientToken || !Regex.IsMatch(value, "^[A-Za-z0-9._:@-]{1,128}$")) throw new PolymorfaValidationException("Invalid server participant.", "invalid_parameter"); + } + private static void Connection(string value) { if (!Regex.IsMatch(value, "^[A-Za-z0-9_-]{8,64}$")) throw new PolymorfaValidationException("Invalid connectionId.", "invalid_parameter"); } + private void Link(string session, RequestOptions? options) + { + http.Credential.RequireServer(); NonEmpty(session, "session"); + if (session.Length > 128 || options?.IdempotencyKey is not null || options?.Headers?.Keys.Any(key => key.Equals("idempotency-key", StringComparison.OrdinalIgnoreCase)) == true) throw new PolymorfaValidationException("Call links require a session and do not support idempotency keys.", "invalid_parameter"); + } + public Task>> CreateCallLinkAsync(CreateCallLinkRequest body, RequestOptions? options = null) + { + Link(body.Session, options); return http.RequestAsync>(HttpMethod.Post, "/messaging/voip/call-links", body, (options ?? new()) with { MaxNetworkRetries = 0 }); + } + public Task>> PreviewCallLinkAsync(PreviewCallLinkRequest body, RequestOptions? options = null) + { + Link(body.Session, options); if (!Regex.IsMatch(body.Token, "^[A-Za-z0-9_-]{1,256}$")) throw new PolymorfaValidationException("Invalid call-link token.", "invalid_parameter"); + return http.RequestAsync>(HttpMethod.Post, "/messaging/voip/call-links/preview", body, (options ?? new()) with { MaxNetworkRetries = 0 }); + } + public Task>> PlaceAsync(PlaceCallRequest body, RequestOptions? options = null) + { + if (http.Credential.Kind != CredentialKind.ClientToken) NonEmpty(body.Session, "session"); + var count = (body.To is not null ? 1 : 0) + (body.Participants is not null ? 1 : 0) + (body.GroupId is not null ? 1 : 0); + if (count != 1 || body.To is not null && string.IsNullOrWhiteSpace(body.To) || body.GroupId is not null && string.IsNullOrWhiteSpace(body.GroupId) || body.Participants is { } people && (people.Count is < 2 or > 31 || people.Any(string.IsNullOrWhiteSpace) || people.Distinct().Count() != people.Count)) throw new PolymorfaValidationException("Provide one valid to, participants, or groupId target.", "invalid_parameter"); + Participant(body.Participant); return http.RequestAsync>(HttpMethod.Post, "/messaging/voip/calls", body, options); + } + public Task>> AcceptAsync(string callId, AcceptCallRequest? body = null, RequestOptions? options = null) { body ??= new(); Participant(body.Participant); return http.RequestAsync>(HttpMethod.Post, Path(callId) + "/accept", body, options); } + public Task> RejectIncomingAsync(string session, string callId, RejectIncomingCallRequest body, RequestOptions? options = null) => http.RequestAsync(HttpMethod.Post, $"/messaging/{Uri.EscapeDataString(session)}/calls/{Uri.EscapeDataString(callId)}/reject", body, options); + public Task> RejectAsync(string callId, RejectCallRequest? body = null, RequestOptions? options = null) { Participant(body?.Participant); return http.RequestAsync(HttpMethod.Post, Path(callId) + "/reject", body?.Participant is null ? null : body, options); } + public Task> LeaveAsync(string callId, LeaveCallRequest body, RequestOptions? options = null) { Connection(body.ConnectionId); Participant(body.Participant); return http.RequestAsync(HttpMethod.Post, Path(callId) + "/leave", body, options); } + public Task> EndAsync(string callId, RequestOptions? options = null) => http.RequestAsync(HttpMethod.Delete, Path(callId), null, options); + public Task>> AddParticipantAsync(string callId, AddCallParticipantRequest body, RequestOptions? options = null) => http.RequestAsync>(HttpMethod.Post, Path(callId) + "/participants", body, options); + public Task> RingParticipantAsync(string callId, AddCallParticipantRequest body, RequestOptions? options = null) => http.RequestAsync(HttpMethod.Post, Path(callId) + "/participants/ring", body, options); + public Task> SendReactionAsync(string callId, CallReactionRequest body, RequestOptions? options = null) + { + Participant(body.Participant); Connection(body.ConnectionId); if (!(new[] { "", "👍", "❤️", "😂", "😮", "😢", "🙏" }).Contains(body.Emoji)) throw new PolymorfaValidationException("Invalid call reaction.", "invalid_parameter"); + return http.RequestAsync(HttpMethod.Post, Path(callId) + "/reaction", body, (options ?? new()) with { MaxNetworkRetries = 0 }); + } + public Task> SetHandRaisedAsync(string callId, CallHandRequest body, RequestOptions? options = null) { Participant(body.Participant); Connection(body.ConnectionId); return http.RequestAsync(HttpMethod.Post, Path(callId) + "/hand", body, (options ?? new()) with { MaxNetworkRetries = 0 }); } + public Task>> RetrieveCallPermissionAsync(string session, string to, RequestOptions? options = null) { http.Credential.RequireServer(); NonEmpty(session, "session"); NonEmpty(to, "to"); return http.RequestAsync>(HttpMethod.Get, $"/messaging/{Uri.EscapeDataString(session)}/call-permissions/{Uri.EscapeDataString(to)}", null, options); } + public Task>> CheckAsync(CheckCallRequest body, RequestOptions? options = null) { http.Credential.RequireServer(); NonEmpty(body.Session, "session"); NonEmpty(body.To, "to"); return http.RequestAsync>(HttpMethod.Post, "/messaging/voip/calls/check", body, options); } + public Task>> RetrieveCallSettingsAsync(string session, RequestOptions? options = null) { http.Credential.RequireServer(); NonEmpty(session, "session"); return http.RequestAsync>(HttpMethod.Get, $"/platform/sessions/{Uri.EscapeDataString(session)}/call-settings", null, options); } + public Task>> UpdateCallSettingsAsync(string session, UpdateCallSettingsRequest body, RequestOptions? options = null) + { + http.Credential.RequireServer(); NonEmpty(session, "session"); + if (body.CallsEnabled is null && body.ConferenceMode is null && body.InboundRoute is null && !body.SipTrunkId.IsSpecified && body.SipClaim is null && body.HostCloudApiCalls is null && body.SimultaneousCalls is null || body.ExpectedRevision < 0 || body.InboundRoute is not null && body.InboundRoute is not ("clients" or "sip_trunk")) throw new PolymorfaValidationException("Invalid call settings update.", "invalid_parameter"); + return http.RequestAsync>(HttpMethod.Put, $"/platform/sessions/{Uri.EscapeDataString(session)}/call-settings", body, options); + } + public Task> ReportAsync(string callId, CallReportRequest body, RequestOptions? options = null) + { + Participant(body.Participant); Connection(body.ConnectionId); + if (!Regex.IsMatch(body.Client.Sdk, "^[a-z0-9@/._-]{1,32}$") || !Regex.IsMatch(body.Client.Version, "^[0-9]{1,6}\\.[0-9]{1,6}\\.[0-9]{1,6}(?:[-+][0-9A-Za-z.+-]{1,24})?$") || body.Client.Platform is not ("browser" or "node" or "other")) throw new PolymorfaValidationException("Invalid call report client.", "invalid_parameter"); + if (body.Kind == "quality" && body.Quality is { } quality && body.Error is null) + { + if (quality == new CallQuality() || quality.RttMs is < 0 or > 60000 || quality.JitterMs is < 0 or > 60000 || quality.PacketsLost < 0 || quality.PacketsReceived < 0 || quality.Reconnects is < 0 or > 1000 || quality.AudioCodec is not null && !Regex.IsMatch(quality.AudioCodec, "^[A-Za-z0-9/.-]{1,32}$") || quality.VideoCodec is not null && !Regex.IsMatch(quality.VideoCodec, "^[A-Za-z0-9/.-]{1,32}$") || quality.CandidateType is not null && quality.CandidateType is not ("host" or "srflx" or "prflx" or "relay")) throw new PolymorfaValidationException("Invalid call quality report.", "invalid_parameter"); + } + else if (body.Kind != "error" || body.Quality is not null || body.Error?.Code is not ("media_permission_denied" or "device_not_found" or "device_in_use" or "ice_failed" or "negotiation_failed" or "media_timeout" or "reconnect_exhausted" or "token_refresh_failed" or "unsupported_browser" or "other")) throw new PolymorfaValidationException("Invalid call error report.", "invalid_parameter"); + return http.RequestAsync(HttpMethod.Post, Path(callId) + "/reports", body, options); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CallsClient.cs b/packages/dotnet/src/Polymorfa.Sdk/CallsClient.cs new file mode 100644 index 00000000..2b5f6093 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CallsClient.cs @@ -0,0 +1,135 @@ +using System.Text.Json; +using System.Threading.Channels; + +namespace Polymorfa.Sdk; + +public sealed record CallsClientOptions +{ + public required string Session { get; init; } + public string Participant { get; init; } = "default"; + public ClientOptions Http { get; init; } = new(); + public CallsSocketOptions? Socket { get; init; } + public int ReconnectAttempts { get; init; } = 3; + public TimeSpan MinBackoff { get; init; } = TimeSpan.FromSeconds(1); + public TimeSpan MaxBackoff { get; init; } = TimeSpan.FromSeconds(30); + public bool Diagnostics { get; init; } = true; +} +public abstract record CallsClientEvent; +public sealed record CallsReady : CallsClientEvent; +public sealed record CallsDisconnected : CallsClientEvent; +public sealed record CallsFailure(Exception Error) : CallsClientEvent; +public sealed record CallKnown(Call Call) : CallsClientEvent; +public sealed record CallIncoming(Call Call) : CallsClientEvent; +public sealed record CallEnded(Call Call, string Reason) : CallsClientEvent; +public sealed record CallChanged(Call Call) : CallsClientEvent; +public sealed record CallLifecycleCandidate(CallCandidate Candidate) : CallsClientEvent; +public sealed record CallCapabilities(bool Video = true, bool Invite = true); +public sealed class CallsClient : IAsyncDisposable +{ + internal CallsTokenSource Tokens { get; } + internal CallsClientOptions Options { get; } + internal CallsSocketOptions SocketOptions => (Options.Socket ?? new() { BaseUrl = Options.Http.BaseUrl }) with { Session = Options.Session, Participant = Options.Participant, Proxy = Options.Socket?.Proxy ?? Options.Http.Proxy }; + private readonly object gate = new(); + private readonly Dictionary calls = []; + private readonly Dictionary> early = []; + private readonly Queue ended = new(); + private readonly Channel events = System.Threading.Channels.Channel.CreateBounded(new BoundedChannelOptions(2048) { FullMode = BoundedChannelFullMode.Wait }); + private readonly CancellationTokenSource lifetime = new(); + private CallsSocket? lifecycle; + private Task? runner; + private readonly TaskCompletionSource firstReady = new(TaskCreationOptions.RunContinuationsAsynchronously); + private int generation; + private int disposed; + public string? SelfParticipant { get; private set; } + public bool Connected => lifecycle?.Connected == true; + public IReadOnlyList ActiveCalls { get { lock (gate) return calls.Values.Where(c => !c.Ended).ToArray(); } } + public CallsClient(string token, CallsClientOptions options) : this(new CallsTokenSource(token), options) { } + public CallsClient(CallsTokenSource tokens, CallsClientOptions options) + { + if (string.IsNullOrWhiteSpace(options.Session) || options.Session.Length > 128 || !CallsProtocol.Participant(options.Participant) || options.ReconnectAttempts < 0 || options.MinBackoff <= TimeSpan.Zero || options.MaxBackoff < options.MinBackoff) throw new PolymorfaConfigurationException("Invalid Calls client configuration."); + Tokens = tokens; Options = options; HttpTransport.ValidateUrl(options.Http.BaseUrl); + } + public IAsyncEnumerable EventsAsync(CancellationToken cancellationToken = default) => events.Reader.ReadAllAsync(cancellationToken); + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + lock (gate) { ObjectDisposedException.ThrowIf(disposed != 0, this); runner ??= RunLifecycleAsync(); } + await firstReady.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + } + private async Task RunLifecycleAsync() + { + await Task.Yield(); var attempt = 0; var refresh = false; + try + { + while (!lifetime.IsCancellationRequested) + { + try + { + await using var connection = await CallsSocket.OpenLifecycleAsync(Tokens, SocketOptions with { Heartbeat = Options.Socket?.Heartbeat ?? TimeSpan.FromSeconds(15) }, refresh, lifetime.Token).ConfigureAwait(false); lifecycle = connection; SelfParticipant = connection.Ready.Participant; attempt = 0; refresh = false; firstReady.TrySetResult(); await PublishAsync(new CallsReady()).ConfigureAwait(false); + await foreach (var frame in connection.FramesAsync(lifetime.Token).ConfigureAwait(false)) + { + if (frame is CallLifecycleEvent e) Receive(e); + else if (frame is CallCandidate candidate) await PublishAsync(new CallLifecycleCandidate(candidate)).ConfigureAwait(false); + else if (frame is CallSocketError error) await PublishAsync(new CallsFailure(new CallsSocketException(error.Message ?? "Calls lifecycle error.", error.Code))).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { break; } + catch (Exception error) + { + refresh = error is CallsSocketException { CloseCode: 4401 }; if (refresh) Tokens.Invalidate(); await PublishAsync(new CallsFailure(error)).ConfigureAwait(false); + if (error is CallsSocketException { CloseCode: 4400 or 1008 }) { firstReady.TrySetException(error); break; } + } + lifecycle = null; await PublishAsync(new CallsDisconnected()).ConfigureAwait(false); + var delay = TimeSpan.FromMilliseconds(Math.Min(Options.MaxBackoff.TotalMilliseconds, Options.MinBackoff.TotalMilliseconds * Math.Pow(2, Math.Min(attempt++, 20)))); await Task.Delay(delay, lifetime.Token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { } + finally { lifecycle = null; firstReady.TrySetCanceled(); } + } + private void Receive(CallLifecycleEvent e) + { + lock (gate) + { + if (calls.TryGetValue(e.CallId, out var call)) { call.Apply(e); return; } + if (e.Event == "call.received") + { + if (Field(e.Payload, "direction") == "outgoing") return; + var peer = Field(e.Payload, "from"); if (peer is null && e.Payload.ValueKind == JsonValueKind.Object && e.Payload.TryGetProperty("from", out var from) && from.ValueKind == JsonValueKind.Object) peer = Field(from, "phoneNumber") ?? Field(from, "id"); + call = Track(new(this, e.CallId, "inbound", peer ?? "", Flag(e.Payload, "hasVideo") || Flag(e.Payload, "has_video"), false)); call.Capabilities = Capabilities(e.Payload, call.Capabilities); if (!call.Ended) Publish(new CallIncoming(call)); return; + } + if (e.Event is not ("call.accepted" or "call.ended" or "call.missed" or "call.rejected" or "call.participant_joined" or "call.participant_state" or "call.participant_left")) return; + if (!early.TryGetValue(e.CallId, out var queue)) { if (early.Count >= 64) early.Remove(early.Keys.First()); early[e.CallId] = queue = []; } + if (queue.Any(x => Terminal(x.Event))) return; + if (Terminal(e.Event)) { queue.Clear(); queue.Add(e); return; } + if (e.Event == "call.accepted" && queue.Any(x => x.Event == e.Event)) return; + if (queue.Count >= 8) { var index = queue.FindIndex(x => x.Event != "call.accepted"); if (index < 0) return; queue.RemoveAt(index); } + queue.Add(e); + } + } + private static bool Terminal(string name) => name is "call.ended" or "call.missed" or "call.rejected"; + private Call Track(Call call) { calls[call.Id] = call; Publish(new CallKnown(call)); if (early.Remove(call.Id, out var queued)) foreach (var e in queued) call.Apply(e); return call; } + public Task PlaceAsync(string to, bool video = false, bool exclusive = false, RequestOptions? options = null) => PlaceAsync(new(To: to, Video: video, Exclusive: exclusive), options); + public Task PlaceGroupAsync(string groupId, bool video = false, RequestOptions? options = null) => PlaceAsync(new(GroupId: groupId, Video: video), options); + public async Task PlaceAsync(PlaceCallRequest input, RequestOptions? options = null) + { + var peer = input.To ?? input.GroupId ?? input.Participants?.FirstOrDefault() ?? ""; + var started = Volatile.Read(ref generation); var response = await RequestAsync(async (client, participant, request) => (await client.Calls.PlaceAsync(input with { Session = participant is null ? null : Options.Session, Participant = participant }, request.WithIdempotency()).ConfigureAwait(false)).Data.Data, options).ConfigureAwait(false); + Call call; lock (gate) { call = calls.GetValueOrDefault(response.CallId) ?? Track(new(this, response.CallId, "outbound", peer, input.Video == true, input.Exclusive == true)); } + if (started != Volatile.Read(ref generation) || lifetime.IsCancellationRequested) { await call.EndAsync().ConfigureAwait(false); throw new CallsSocketException("Calls client disconnected during placement.", "client_disconnected"); } + return call; + } + internal async Task RequestAsync(Func> operation, RequestOptions? options = null) + { + options ??= new(); var token = await Tokens.GetAsync(cancellationToken: options.CancellationToken).ConfigureAwait(false); var credential = token.Value.StartsWith("pmfa_ct_", StringComparison.Ordinal) ? Credential.ClientToken(token.Value) : token.Value.StartsWith("pmfa_pt_", StringComparison.Ordinal) ? Credential.ProjectToken(token.Value) : Credential.OrganizationApiKey(token.Value); + using var client = new MessagingClient(credential, Options.Http); return await operation(client, credential.Kind == CredentialKind.ClientToken ? null : Options.Participant, options).ConfigureAwait(false); + } + internal async Task PublishAsync(CallsClientEvent value) { try { await events.Writer.WriteAsync(value, lifetime.Token).ConfigureAwait(false); } catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { } } + internal void Publish(CallsClientEvent value) { if (!events.Writer.TryWrite(value)) { events.Writer.TryComplete(new CallsSocketException("Calls event consumer fell behind the bounded queue.", "event_backpressure")); lifetime.Cancel(); } } + internal void Completed(Call call, string reason) { lock (gate) { ended.Enqueue(call.Id); while (ended.Count > 200) { var id = ended.Dequeue(); if (calls.TryGetValue(id, out var old) && old.Ended) calls.Remove(id); } } Publish(new CallEnded(call, reason)); } + internal static string? Field(JsonElement value, string key) => value.ValueKind == JsonValueKind.Object && value.TryGetProperty(key, out var x) && x.ValueKind == JsonValueKind.String ? x.GetString() : null; + internal static bool Flag(JsonElement value, string key) => value.ValueKind == JsonValueKind.Object && value.TryGetProperty(key, out var x) && x.ValueKind == JsonValueKind.True; + internal static CallCapabilities Capabilities(JsonElement payload, CallCapabilities fallback) { if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty("capabilities", out var c) || c.ValueKind != JsonValueKind.Object) return fallback; bool Value(string key, bool old) => c.TryGetProperty(key, out var x) && x.ValueKind is JsonValueKind.True or JsonValueKind.False ? x.GetBoolean() : old; return new(Value("video", fallback.Video), Value("invite", fallback.Invite)); } + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref disposed, 1) != 0) return; Interlocked.Increment(ref generation); lifetime.Cancel(); if (lifecycle is { } socket) await socket.DisposeAsync().ConfigureAwait(false); if (runner is not null) await runner.ConfigureAwait(false); foreach (var call in ActiveCalls) await call.DisconnectAsync().ConfigureAwait(false); events.Writer.TryComplete(); lifetime.Dispose(); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CallsProtocol.cs b/packages/dotnet/src/Polymorfa.Sdk/CallsProtocol.cs new file mode 100644 index 00000000..0f572d37 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CallsProtocol.cs @@ -0,0 +1,75 @@ +using System.Buffers.Binary; +using System.Text.Json; + +namespace Polymorfa.Sdk; + +public abstract record CallSocketFrame; +public sealed record CallReady(string? Session, string? Participant, int? SampleRate, bool? Video, string? CallId, string? ConnectionId) : CallSocketFrame; +public sealed record CallLifecycleEvent(string Event, string CallId, JsonElement Payload, string Timestamp) : CallSocketFrame; +public sealed record CallTrickleCandidate(string Candidate, string? SdpMid = null, double? SdpMLineIndex = null); +public sealed record CallCandidate(string CallId, string? ConnectionId, CallTrickleCandidate Candidate) : CallSocketFrame; +public sealed record CallSocketError(string Code, string? Message) : CallSocketFrame; +public sealed record CallPong : CallSocketFrame; +public sealed record CallAudioFrame(short[] Pcm) : CallSocketFrame; +public sealed record CallVideoFrame(byte Codec, bool Keyframe, uint Source, ulong TimestampUs, byte[] Data) : CallSocketFrame; +public sealed record CallMediaState(bool AudioMuted, bool VideoEnabled, bool ScreenSharing = false); +public sealed record CallMediaStateReply(string RequestId, bool AudioMuted, bool VideoEnabled, bool ScreenSharing = false) : CallSocketFrame; +public sealed record CallMediaStateError(string RequestId, string Code) : CallSocketFrame; +public sealed record CallRemoteMedia(bool? AudioMuted) : CallSocketFrame; +public sealed record CallReaction(string Emoji, bool? Self, string? ParticipantId) : CallSocketFrame; +public sealed record CallHandState(bool Raised, bool Supported) : CallSocketFrame; +public sealed record CallParticipantFrame(string Type, CallParticipant Participant) : CallSocketFrame; +public sealed record CallParticipantLeft(string ParticipantId, string? Reason) : CallSocketFrame; +public sealed record CallVideoSource(uint Source, CallParticipant? Participant, string? ConnectionId, string? ConnectionParticipant) : CallSocketFrame; +public sealed record CallVideoSourceRemoved(uint Source) : CallSocketFrame; +public sealed record CallKeyframeRequest : CallSocketFrame; +public static class CallsProtocol +{ + public const string MediaSubprotocol = "pmfa.calls.v2"; + public static string CreateConnectionId() => Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(18)).Replace('+', '-').Replace('/', '_').TrimEnd('='); + internal static bool Connection(string? value) => value is not null && System.Text.RegularExpressions.Regex.IsMatch(value, "^[A-Za-z0-9_-]{8,64}$"); + internal static bool Participant(string? value) => value is not null && System.Text.RegularExpressions.Regex.IsMatch(value, "^[A-Za-z0-9._:@-]{1,128}$"); + public static byte[] EncodeAudio(ReadOnlySpan pcm) { var bytes = new byte[1 + pcm.Length * 2]; bytes[0] = 1; for (var i = 0; i < pcm.Length; i++) BinaryPrimitives.WriteInt16LittleEndian(bytes.AsSpan(1 + i * 2), pcm[i]); return bytes; } + public static byte[] EncodeVideo(CallVideoFrame frame) { if (frame.Codec != 1 || frame.Data.Length == 0) throw new PolymorfaValidationException("Provide H.264 video data.", "invalid_media_frame"); var bytes = new byte[15 + frame.Data.Length]; bytes[0] = 2; bytes[1] = 1; bytes[2] = frame.Keyframe ? (byte)1 : (byte)0; BinaryPrimitives.WriteUInt32BigEndian(bytes.AsSpan(3), frame.Source); BinaryPrimitives.WriteUInt64BigEndian(bytes.AsSpan(7), frame.TimestampUs); frame.Data.CopyTo(bytes, 15); return bytes; } + public static CallSocketFrame? DecodeBinary(ReadOnlySpan bytes) + { + if (bytes.Length < 1) return null; + if (bytes[0] == 1) { if (bytes.Length % 2 != 1) return null; var pcm = new short[(bytes.Length - 1) / 2]; for (var i = 0; i < pcm.Length; i++) pcm[i] = BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(1 + i * 2, 2)); return new CallAudioFrame(pcm); } + if (bytes[0] != 2 || bytes.Length <= 15 || bytes[1] != 1) return null; + return new CallVideoFrame(bytes[1], (bytes[2] & 1) != 0, BinaryPrimitives.ReadUInt32BigEndian(bytes[3..]), BinaryPrimitives.ReadUInt64BigEndian(bytes[7..]), bytes[15..].ToArray()); + } + public static CallSocketFrame? ParseText(ReadOnlySpan bytes, bool media) + { + try + { + using var doc = JsonDocument.Parse(bytes.ToArray()); var f = doc.RootElement; if (f.ValueKind != JsonValueKind.Object) return null; + string? S(string name) => f.TryGetProperty(name, out var x) && x.ValueKind == JsonValueKind.String ? x.GetString() : null; + bool? B(string name) => f.TryGetProperty(name, out var x) && x.ValueKind is JsonValueKind.True or JsonValueKind.False ? x.GetBoolean() : null; + uint U(string name) => f.TryGetProperty(name, out var x) && x.TryGetUInt32(out var n) ? n : 0; + bool OptionalString(string name) => !f.TryGetProperty(name, out var value) || value.ValueKind == JsonValueKind.String; + bool OptionalBool(string name) => !f.TryGetProperty(name, out var value) || value.ValueKind is JsonValueKind.True or JsonValueKind.False; + var type = S("type"); var id = S("requestId"); + if (type == "pong") return new CallPong(); + if (type == "error" && S("code") is { } code && OptionalString("message")) return new CallSocketError(code, S("message")); + if (type == "ready") { var rate = U("sampleRate"); return ((!media && OptionalString("session") && OptionalString("participant")) || (media && rate is > 0 and <= int.MaxValue && B("video") is not null && OptionalString("callId") && OptionalString("connectionId"))) ? new CallReady(S("session"), S("participant"), media ? (int)rate : null, B("video"), S("callId"), S("connectionId")) : null; } + if (!media) + { + if (type == "event" && S("event") is { } name && S("callId") is { } call && S("timestamp") is { } timestamp && f.TryGetProperty("payload", out var payload)) return new CallLifecycleEvent(name, call, payload.Clone(), timestamp); + if (type == "candidate" && S("callId") is { } callId && (S("connectionId") is null || Connection(S("connectionId"))) && f.TryGetProperty("candidate", out var candidate) && candidate.ValueKind == JsonValueKind.Object && candidate.TryGetProperty("candidate", out var text) && text.ValueKind == JsonValueKind.String) return new CallCandidate(callId, S("connectionId"), candidate.Deserialize(HttpTransport.Json)!); + return null; + } + if (type == "media_state" && Connection(id) && B("audioMuted") is { } muted && B("videoEnabled") is { } enabled && OptionalBool("screenSharing")) return new CallMediaStateReply(id!, muted, enabled, B("screenSharing") == true); + if (type == "media_error" && Connection(id) && S("code") is { } error) return new CallMediaStateError(id!, error); + if (type == "remote_media" && f.TryGetProperty("audioMuted", out var remote) && remote.ValueKind is JsonValueKind.True or JsonValueKind.False or JsonValueKind.Null) return new CallRemoteMedia(B("audioMuted")); + if (type == "reaction" && S("emoji") is { } emoji && new[] { "", "👍", "❤️", "😂", "😮", "😢", "🙏" }.Contains(emoji) && (B("self") == true && !f.TryGetProperty("participantId", out _) || !f.TryGetProperty("self", out _) && S("participantId") is { } participantId && System.Text.RegularExpressions.Regex.IsMatch(participantId, "^[1-9][0-9]{0,18}$"))) return new CallReaction(emoji, B("self"), S("participantId")); + if (type == "hand_state" && B("raised") is { } raised && B("supported") is { } supported) return new CallHandState(raised, supported); + if (type == "participant_left" && S("participantId") is { } left && OptionalString("reason")) return new CallParticipantLeft(left, S("reason")); + CallParticipant? ParticipantValue() { if (!f.TryGetProperty("participant", out var p) || p.ValueKind != JsonValueKind.Object) return null; if (p.TryGetProperty("handle", out _) || !p.TryGetProperty("audioMuted", out var audio) || audio.ValueKind is not (JsonValueKind.True or JsonValueKind.False) || !p.TryGetProperty("video", out var video) || video.ValueKind is not (JsonValueKind.True or JsonValueKind.False) || new[] { "phoneNumber", "bsuid", "username" }.Any(key => p.TryGetProperty(key, out var v) && v.ValueKind != JsonValueKind.String) || p.TryGetProperty("handRaised", out var hand) && hand.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) return null; var value = p.Deserialize(HttpTransport.Json); return value is { Id.Length: > 0, State: "invited" or "ringing" or "connected" or "left" } ? value : null; } + if (type is "participant_joined" or "participant_state" && ParticipantValue() is { } participant) return new CallParticipantFrame(type, participant); + if (type == "video_source" && U("source") > 0) { var owner = ParticipantValue(); var connection = S("connectionId"); if (owner is not null && !f.TryGetProperty("connectionId", out _) && !f.TryGetProperty("connectionParticipant", out _) || !f.TryGetProperty("participant", out _) && Connection(connection) && OptionalString("connectionParticipant")) return new CallVideoSource(U("source"), owner, connection, S("connectionParticipant")); } + if (type == "video_source_removed" && U("source") > 0) return new CallVideoSourceRemoved(U("source")); + return type == "keyframe_request" ? new CallKeyframeRequest() : null; + } + catch (Exception e) when (e is JsonException or InvalidOperationException or FormatException) { return null; } + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CallsSocket.cs b/packages/dotnet/src/Polymorfa.Sdk/CallsSocket.cs new file mode 100644 index 00000000..be3b3818 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CallsSocket.cs @@ -0,0 +1,160 @@ +using System.Net; +using System.Net.WebSockets; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading.Channels; + +namespace Polymorfa.Sdk; + +public sealed class CallsSocketException(string message, string code, int? closeCode = null) : PolymorfaException(message, code) { public int? CloseCode { get; } = closeCode; } +public sealed record CallsSocketOptions +{ + public Uri BaseUrl { get; init; } = new("https://api.polymorfa.com"); + public IWebProxy? Proxy { get; init; } + public string? Session { get; init; } + public string? Participant { get; init; } + public TimeSpan ReadyTimeout { get; init; } = TimeSpan.FromSeconds(10); + public TimeSpan Heartbeat { get; init; } = TimeSpan.FromSeconds(5); + public int MaxFrameBytes { get; init; } = 4 * 1024 * 1024; +} +/// One authenticated connection. Consume FramesAsync from one reader; writes are serialized independently. +public sealed class CallsSocket : IAsyncDisposable +{ + private readonly ClientWebSocket socket = new(); + private readonly CallsTokenSource tokens; + private readonly CallsSocketOptions options; + private readonly bool media; + private readonly string? connectionId; + private readonly Channel frames = System.Threading.Channels.Channel.CreateBounded(new BoundedChannelOptions(256) { FullMode = BoundedChannelFullMode.Wait, SingleReader = true, SingleWriter = true }); + private readonly CancellationTokenSource lifetime = new(); + private readonly SemaphoreSlim writes = new(1, 1); + private readonly SemaphoreSlim controls = new(1, 1); + private readonly object controlGate = new(); + private TaskCompletionSource? pendingControl; + private string? pendingId; + private int queuedControls; + private int awaitingPong; + private Task? reader; + private Task? heartbeat; + private CallsToken token = null!; + private int disposed; + public CallReady Ready { get; private set; } = null!; + public string? ConnectionId => connectionId; + public int? CloseCode => socket.CloseStatus is { } code ? (int)code : null; + public bool Connected => socket.State == WebSocketState.Open && !lifetime.IsCancellationRequested; + private CallsSocket(CallsTokenSource tokens, CallsSocketOptions options, bool media, string? connectionId) + { + this.tokens = tokens; this.options = options; this.media = media; this.connectionId = connectionId; + HttpTransport.ValidateUrl(options.BaseUrl); if (options.ReadyTimeout <= TimeSpan.Zero || options.Heartbeat < TimeSpan.Zero || options.MaxFrameBytes < 1024) throw new PolymorfaConfigurationException("Invalid Calls socket limits."); + if (options.Participant is not null && !CallsProtocol.Participant(options.Participant)) throw new PolymorfaConfigurationException("Invalid participant."); + if (media && !CallsProtocol.Connection(connectionId)) throw new PolymorfaConfigurationException("Invalid connectionId."); + } + public static Task OpenLifecycleAsync(CallsTokenSource tokens, CallsSocketOptions? options = null, bool refresh = false, CancellationToken cancellationToken = default) => OpenAsync(tokens, options ?? new(), null, null, refresh, cancellationToken); + public static Task OpenMediaAsync(CallsTokenSource tokens, string callId, string connectionId, CallsSocketOptions? options = null, bool refresh = false, CancellationToken cancellationToken = default) => OpenAsync(tokens, options ?? new(), callId, connectionId, refresh, cancellationToken); + private static async Task OpenAsync(CallsTokenSource tokens, CallsSocketOptions options, string? callId, string? connectionId, bool refresh, CancellationToken cancellation) + { + var result = new CallsSocket(tokens, options, callId is not null, connectionId); + using var bound = CancellationTokenSource.CreateLinkedTokenSource(cancellation); bound.CancelAfter(options.ReadyTimeout); + try + { + result.token = await tokens.GetAsync(refresh, bound.Token).ConfigureAwait(false); + var clientToken = result.token.Value.StartsWith("pmfa_ct_", StringComparison.Ordinal); + if (!clientToken) { if (OperatingSystem.IsBrowser()) throw new PolymorfaConfigurationException("Server Calls credentials cannot be used in a browser."); if (callId is null && (string.IsNullOrWhiteSpace(options.Session) || options.Session.Length > 128)) throw new PolymorfaConfigurationException("Server lifecycle sockets require a session."); } + var path = callId is null ? "/voip/ws" : "/voip/calls/" + Uri.EscapeDataString(callId) + "/media"; + var uri = new UriBuilder(new Uri(options.BaseUrl.AbsoluteUri.TrimEnd('/') + path)) { Scheme = options.BaseUrl.Scheme == "https" ? "wss" : "ws" }; + if (callId is null && !clientToken) uri.Query = "session=" + Uri.EscapeDataString(options.Session!) + (options.Participant is null ? "" : "&participant=" + Uri.EscapeDataString(options.Participant)); + if (callId is not null) result.socket.Options.AddSubProtocol(CallsProtocol.MediaSubprotocol); + result.socket.Options.KeepAliveInterval = TimeSpan.Zero; + if (options.Proxy is not null) result.socket.Options.Proxy = options.Proxy; + await result.socket.ConnectAsync(uri.Uri, bound.Token).ConfigureAwait(false); + await result.AuthenticateAsync(bound.Token).ConfigureAwait(false); + while (true) + { + var frame = await result.ReceiveAsync(bound.Token).ConfigureAwait(false); + if (frame is CallReady ready) { result.Ready = ready; break; } + if (frame is CallSocketError error) throw new CallsSocketException("Calls socket authentication was refused.", error.Code, error.Code switch { "call_claimed" => 4409, "calls_disabled" => 4403, "unauthorized" => 4401, _ => null }); + if (frame is not null) await result.frames.Writer.WriteAsync(frame, bound.Token).ConfigureAwait(false); + } + result.reader = result.ReadLoopAsync(); result.heartbeat = result.HeartbeatAsync(); return result; + } + catch (OperationCanceledException) { await result.DisposeAsync().ConfigureAwait(false); if (cancellation.IsCancellationRequested) throw new PolymorfaCancelledException(); throw new CallsSocketException("Calls authentication timed out.", "media_timeout"); } + catch { await result.DisposeAsync().ConfigureAwait(false); throw; } + } + private Task AuthenticateAsync(CancellationToken cancellation) => SendObjectAsync(new { type = "auth", token = token.Value, connectionId, participant = media && !token.Value.StartsWith("pmfa_ct_", StringComparison.Ordinal) ? options.Participant : null }, cancellation); + public IAsyncEnumerable FramesAsync(CancellationToken cancellationToken = default) => frames.Reader.ReadAllAsync(cancellationToken); + private async Task ReceiveAsync(CancellationToken cancellation) + { + var buffer = new byte[8192]; using var output = new MemoryStream(); ValueWebSocketReceiveResult part; WebSocketMessageType? kind = null; + do { part = await socket.ReceiveAsync(buffer.AsMemory(), cancellation).ConfigureAwait(false); if (part.MessageType == WebSocketMessageType.Close) throw new CallsSocketException("Calls socket closed.", CloseCode == 4401 ? "token_refresh_failed" : "socket_closed", CloseCode); if (kind is not null && kind != part.MessageType) throw new CallsSocketException("Invalid fragmented Calls frame.", "invalid_media_frame"); kind = part.MessageType; if (output.Length + part.Count > options.MaxFrameBytes) throw new CallsSocketException("Calls frame exceeds the configured limit.", "invalid_media_frame"); output.Write(buffer, 0, part.Count); } while (!part.EndOfMessage); + return kind == WebSocketMessageType.Binary ? media ? CallsProtocol.DecodeBinary(output.ToArray()) : null : CallsProtocol.ParseText(output.ToArray(), media); + } + private async Task ReadLoopAsync() + { + Exception? failure = null; + try + { + while (!lifetime.IsCancellationRequested) + { + var frame = await ReceiveAsync(lifetime.Token).ConfigureAwait(false); if (frame is null) continue; + if (frame is CallPong) { Interlocked.Exchange(ref awaitingPong, 0); continue; } + lock (controlGate) + { + if (frame is CallMediaStateReply reply && reply.RequestId == pendingId) pendingControl?.TrySetResult(new(reply.AudioMuted, reply.VideoEnabled, reply.ScreenSharing)); + else if (frame is CallMediaStateError error && error.RequestId == pendingId) pendingControl?.TrySetException(new CallsSocketException("Media state change was refused.", error.Code)); + } + await frames.Writer.WriteAsync(frame, lifetime.Token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { } + catch (Exception e) { failure = e; } + finally { lifetime.Cancel(); lock (controlGate) pendingControl?.TrySetException(failure ?? new CallsSocketException("Media connection closed.", "media_control_unavailable")); frames.Writer.TryComplete(failure); } + } + private async Task HeartbeatAsync() + { + try + { + var interval = options.Heartbeat == TimeSpan.Zero ? TimeSpan.FromSeconds(5) : options.Heartbeat; + while (!lifetime.IsCancellationRequested) + { + await Task.Delay(interval, lifetime.Token).ConfigureAwait(false); + if (options.Heartbeat > TimeSpan.Zero) { if (Interlocked.Exchange(ref awaitingPong, 1) != 0) throw new CallsSocketException("Calls heartbeat timed out.", "media_timeout"); await SendObjectAsync(new { type = "ping" }, lifetime.Token).ConfigureAwait(false); } + if (token.ExpiresAt is { } expiry && expiry - TimeSpan.FromSeconds(60) <= DateTimeOffset.UtcNow) { token = await tokens.GetAsync(true, lifetime.Token).ConfigureAwait(false); await AuthenticateAsync(lifetime.Token).ConfigureAwait(false); } + } + } + catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { } + catch (Exception error) { frames.Writer.TryComplete(error); lifetime.Cancel(); socket.Abort(); } + } + private async Task SendAsync(byte[] data, WebSocketMessageType type, CancellationToken cancellation) + { + using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellation, lifetime.Token); await writes.WaitAsync(linked.Token).ConfigureAwait(false); + try { await socket.SendAsync(data.AsMemory(), type, true, linked.Token).ConfigureAwait(false); } + finally { writes.Release(); } + } + private Task SendObjectAsync(object frame, CancellationToken cancellation = default) => SendAsync(JsonSerializer.SerializeToUtf8Bytes(frame, HttpTransport.Json), WebSocketMessageType.Text, cancellation); + public Task SendAudioAsync(short[] pcm, CancellationToken cancellationToken = default) { if (!media) throw new PolymorfaConfigurationException("Audio requires a media connection."); return SendAsync(CallsProtocol.EncodeAudio(pcm), WebSocketMessageType.Binary, cancellationToken); } + public Task SendVideoAsync(CallVideoFrame frame, CancellationToken cancellationToken = default) { if (!media) throw new PolymorfaConfigurationException("Video requires a media connection."); return SendAsync(CallsProtocol.EncodeVideo(frame with { Source = 0 }), WebSocketMessageType.Binary, cancellationToken); } + public Task SendCandidateAsync(string callId, string connectionId, CallTrickleCandidate candidate, CancellationToken cancellationToken = default) { if (media || !CallsProtocol.Connection(connectionId)) throw new PolymorfaConfigurationException("Candidates require a lifecycle connection and valid connectionId."); return SendObjectAsync(new { type = "candidate", callId, connectionId, candidate }, cancellationToken); } + public async Task SetMediaStateAsync(bool? audioMuted = null, bool? videoEnabled = null, bool? screenSharing = null, CancellationToken cancellationToken = default) + { + if (!media || audioMuted is null && videoEnabled is null && screenSharing is null) throw new PolymorfaValidationException("Specify a media preference.", "invalid_parameter"); + if (Interlocked.Increment(ref queuedControls) > 16) { Interlocked.Decrement(ref queuedControls); throw new CallsSocketException("Too many pending media commands.", "media_control_unavailable"); } + var entered = false; + try + { + await controls.WaitAsync(cancellationToken).ConfigureAwait(false); entered = true; var id = CallsProtocol.CreateConnectionId(); var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); lock (controlGate) { pendingId = id; pendingControl = completion; } + await SendObjectAsync(new { type = "media_state", requestId = id, audioMuted, videoEnabled, screenSharing }, cancellationToken).ConfigureAwait(false); + var reply = await completion.Task.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false); + if (audioMuted is not null && audioMuted != reply.AudioMuted || videoEnabled is not null && videoEnabled != reply.VideoEnabled || screenSharing is not null && screenSharing != reply.ScreenSharing) throw new CallsSocketException("Media state acknowledgement differs from the request.", "media_control_unknown"); return reply; + } + catch (TimeoutException) { throw new CallsSocketException("Media command timed out; its effect is unknown.", "media_control_unknown"); } + finally { if (entered) { lock (controlGate) { pendingId = null; pendingControl = null; } controls.Release(); } Interlocked.Decrement(ref queuedControls); } + } + public Task LeaveAsync(CancellationToken cancellationToken = default) => SendObjectAsync(new { type = "leave" }, cancellationToken); + public Task EndCallAsync(CancellationToken cancellationToken = default) => SendObjectAsync(new { type = "end_call" }, cancellationToken); + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref disposed, 1) != 0) return; + lifetime.Cancel(); socket.Abort(); frames.Writer.TryComplete(); + if (reader is not null) await reader.ConfigureAwait(false); if (heartbeat is not null) await heartbeat.ConfigureAwait(false); socket.Dispose(); lifetime.Dispose(); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CallsTokenSource.cs b/packages/dotnet/src/Polymorfa.Sdk/CallsTokenSource.cs new file mode 100644 index 00000000..6db87796 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CallsTokenSource.cs @@ -0,0 +1,45 @@ +namespace Polymorfa.Sdk; + +public sealed record CallsToken(string Value, DateTimeOffset? ExpiresAt = null) { public override string ToString() => "CallsToken(redacted)"; } +public sealed record CallsTokenRequest(bool Refresh, CancellationToken CancellationToken); +public delegate Task CallsTokenProvider(CallsTokenRequest request); +public sealed class CallsTokenSource +{ + private sealed class Pending(bool refresh, long generation) + { + internal bool Refresh { get; } = refresh; + internal long Generation { get; } = generation; + internal CancellationTokenSource Cancellation { get; } = new(); + internal Task Task { get; set; } = null!; + internal int Waiters; + } + private readonly object gate = new(); + private readonly CallsTokenProvider provider; + private readonly TimeSpan skew; + private CallsToken? cached; + private Pending? pending; + private long generation; + public CallsTokenSource(string token, TimeSpan? expirySkew = null) : this(_ => Task.FromResult(new CallsToken(token)), expirySkew) { Validate(new(token)); } + public CallsTokenSource(CallsTokenProvider provider, TimeSpan? expirySkew = null) { this.provider = provider ?? throw new ArgumentNullException(nameof(provider)); skew = expirySkew ?? TimeSpan.FromSeconds(30); if (skew < TimeSpan.Zero) throw new PolymorfaConfigurationException("Expiry skew cannot be negative."); } + public async Task GetAsync(bool refresh = false, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); Pending entry; + lock (gate) + { + if (!refresh && cached is { } token && (token.ExpiresAt is null || token.ExpiresAt - skew > DateTimeOffset.UtcNow)) return token; + if (refresh) cached = null; + if (pending is null || refresh && !pending.Refresh) { entry = new(refresh, ++generation); pending = entry; entry.Task = FetchAsync(entry); } else entry = pending; + entry.Waiters++; + } + try { return await entry.Task.WaitAsync(cancellationToken).ConfigureAwait(false); } + finally { lock (gate) { entry.Waiters--; if (entry.Waiters == 0 && !entry.Task.IsCompleted) { if (pending == entry) { pending = null; generation++; } entry.Cancellation.Cancel(); } } } + } + private async Task FetchAsync(Pending entry) + { + await Task.Yield(); + try { var token = await provider(new(entry.Refresh, entry.Cancellation.Token)).ConfigureAwait(false); Validate(token); lock (gate) { if (entry.Generation == generation) cached = token; } return token; } + finally { lock (gate) { if (pending == entry) pending = null; } } + } + public void Invalidate() { lock (gate) { cached = null; pending = null; generation++; } } + private static void Validate(CallsToken token) { if (token is null || string.IsNullOrWhiteSpace(token.Value) || token.Value.Contains('\r') || token.Value.Contains('\n')) throw new PolymorfaConfigurationException("The Calls token provider returned an invalid token."); } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CampaignContractCodec.cs b/packages/dotnet/src/Polymorfa.Sdk/CampaignContractCodec.cs new file mode 100644 index 00000000..acc1e011 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CampaignContractCodec.cs @@ -0,0 +1,109 @@ +using System.Reflection; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +namespace Polymorfa.Sdk; + +internal sealed record CampaignCapturedBody(byte[] Bytes); +internal static class CampaignContractCodec +{ + // New contract helpers capture their own compact UTF-8 bytes once. Legacy transport + // callers retain HttpTransport.Json's established escaping policy. + private static readonly JsonSerializerOptions WireJson = new(HttpTransport.Json) { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + internal static byte[] Encode(JsonElement value) => JsonSerializer.SerializeToUtf8Bytes(value, WireJson); + internal static async Task> Request(HttpTransport http, HttpMethod method, string path, object? body = null, RequestOptions? options = null, IReadOnlyList>? query = null) + { + var captured = body is null ? null : new CampaignCapturedBody(Encode((JsonElement)body)); + var response = await http.RequestAsync(method, path, captured, options, query).ConfigureAwait(false); + try + { + if (response.Metadata.Status != 200 || response.Metadata.Headers.GetValueOrDefault("content-type")?.Split(';')[0].Trim() is not string mime || !mime.Equals("application/json", StringComparison.OrdinalIgnoreCase)) throw new JsonException(); + Validate(response.Data, typeof(T), false); + return new(response.Data.Deserialize(HttpTransport.Json) ?? throw new JsonException(), response.Metadata); + } + catch (Exception error) when (error is JsonException or InvalidOperationException) + { + throw new PolymorfaServerException("Invalid API response structure.", "invalid_response", response.Metadata); + } + } + internal static void Validate(JsonElement value, Type type, bool nullable) + { + var underlying = Nullable.GetUnderlyingType(type); + if (underlying is not null) + { + type = underlying; + nullable = true; + } + if (value.ValueKind == JsonValueKind.Null) + { + if (!nullable && type != typeof(AudienceProfileAttributeValue)) throw new JsonException(); + return; + } + if (type == typeof(JsonElement)) return; + if (type == typeof(CampaignReplyFlowSend) || type == typeof(CampaignReplyFlowMatch)) type = CampaignReplyFlowUnions.Resolve(type, value); + if (type == typeof(AudienceProfileAttributeValue)) + { + AudienceProfileAttributeValue.From(value); + return; + } + if (type == typeof(CampaignVariableValue)) + { + CampaignVariableValue.From(value); + return; + } + if (type == typeof(string)) + { + if (value.ValueKind != JsonValueKind.String) throw new JsonException(); + return; + } + if (type == typeof(bool)) + { + if (value.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) throw new JsonException(); + return; + } + if (type == typeof(long) || type == typeof(int)) + { + if (value.ValueKind != JsonValueKind.Number || !value.TryGetInt64(out var number) || Math.Abs((double)number) > 9007199254740991 || type == typeof(int) && !value.TryGetInt32(out _)) throw new JsonException(); + return; + } + if (type.IsEnum) + { + if (value.ValueKind != JsonValueKind.String) throw new JsonException(); + JsonSerializer.Deserialize(value.GetRawText(), type, HttpTransport.Json); + return; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)) + { + if (value.ValueKind != JsonValueKind.Array) throw new JsonException(); + foreach (var item in value.EnumerateArray()) Validate(item, type.GetGenericArguments()[0], false); + return; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) + { + if (value.ValueKind != JsonValueKind.Object) throw new JsonException(); + foreach (var item in value.EnumerateObject()) Validate(item.Value, type.GetGenericArguments()[1], false); + return; + } + if (value.ValueKind != JsonValueKind.Object) throw new JsonException(); + var context = new NullabilityInfoContext(); + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + var name = property.GetCustomAttribute()?.Name ?? JsonNamingPolicy.CamelCase.ConvertName(property.Name); + if (!value.TryGetProperty(name, out var item)) + { + if (property.GetCustomAttribute() is not null) continue; + throw new JsonException(); + } + var optional = property.GetCustomAttribute() is not null; + var envelopeData = name == "data" && type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(DataEnvelope<>) || type.GetGenericTypeDefinition() == typeof(SuccessEnvelope<>)); + Validate(item, property.PropertyType, !optional && !envelopeData && context.Create(property).ReadState == NullabilityState.Nullable); + if (property.GetCustomAttribute() is { } literal && (item.ValueKind != JsonValueKind.String || !literal.Values.Contains(item.GetString()))) throw new JsonException(); + } + if (type == typeof(CampaignReplyKeyword) && value.GetProperty("value").GetString()!.Length > 64) throw new JsonException(); + if (type == typeof(CampaignReplyFlowDefinition) && CampaignContractValidation.SemanticBytes(value.GetProperty("nodes")) > 24 * 1024) throw new JsonException(); + if (type == typeof(StopConfirmationSettings) && value.GetProperty("textRevision").GetInt32() != 1) throw new JsonException(); + if (type == typeof(CampaignLaunchApproval) && value.GetProperty("reasons").EnumerateArray().Any(reason => reason.GetString() is not ("audience_threshold" or "agent_request"))) throw new JsonException(); + if (type == typeof(DeleteAudienceProfileResult) && !value.GetProperty("deleted").GetBoolean()) throw new JsonException(); + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(SuccessEnvelope<>) && !value.GetProperty("success").GetBoolean()) throw new JsonException(); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CampaignContractModels.cs b/packages/dotnet/src/Polymorfa.Sdk/CampaignContractModels.cs new file mode 100644 index 00000000..487f0195 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CampaignContractModels.cs @@ -0,0 +1,213 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +[AttributeUsage(AttributeTargets.Property)] internal sealed class CampaignOptionalAttribute : Attribute; +[AttributeUsage(AttributeTargets.Property)] +internal sealed class CampaignLiteralAttribute(params string[] values) : Attribute +{ + internal string[] Values + { + get; + } = values; +} + +public abstract class CampaignStringEnumConverter(params (T Value, string Wire)[] entries) : JsonConverter where T : struct, Enum +{ + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) throw new JsonException("Unsupported enum value."); + var text = reader.GetString(); + foreach (var entry in entries) if (entry.Wire == text) return entry.Value; + throw new JsonException("Unsupported enum value."); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var found = entries.Where(e => EqualityComparer.Default.Equals(e.Value, value)).ToArray(); + if (found.Length != 1) throw new JsonException("Unsupported enum value."); + writer.WriteStringValue(found[0].Wire); + } +} +[JsonConverter(typeof(SignalCustodyModeConverter))] public enum SignalCustodyMode { Standard, ZeroKnowledge } +public sealed class SignalCustodyModeConverter() : CampaignStringEnumConverter((SignalCustodyMode.Standard, "standard"), (SignalCustodyMode.ZeroKnowledge, "zero_knowledge")); +[JsonConverter(typeof(AudienceProfileSourceConverter))] public enum AudienceProfileSource { Api, Csv, Form } +public sealed class AudienceProfileSourceConverter() : CampaignStringEnumConverter((AudienceProfileSource.Api, "api"), (AudienceProfileSource.Csv, "csv"), (AudienceProfileSource.Form, "form")); +[JsonConverter(typeof(StopConfirmationLocaleConverter))] public enum StopConfirmationLocale { En, PtBr, Es } +public sealed class StopConfirmationLocaleConverter() : CampaignStringEnumConverter((StopConfirmationLocale.En, "en"), (StopConfirmationLocale.PtBr, "pt-BR"), (StopConfirmationLocale.Es, "es")); +[JsonConverter(typeof(CampaignRetargetOutcomeConverter))] public enum CampaignRetargetOutcome { Delivered, NotDelivered, Read, NotRead, Replied, NotReplied, Failed } +public sealed class CampaignRetargetOutcomeConverter() : CampaignStringEnumConverter((CampaignRetargetOutcome.Delivered, "delivered"), (CampaignRetargetOutcome.NotDelivered, "not_delivered"), (CampaignRetargetOutcome.Read, "read"), (CampaignRetargetOutcome.NotRead, "not_read"), (CampaignRetargetOutcome.Replied, "replied"), (CampaignRetargetOutcome.NotReplied, "not_replied"), (CampaignRetargetOutcome.Failed, "failed")); +[JsonConverter(typeof(CampaignRecipientFailureReasonConverter))] public enum CampaignRecipientFailureReason { OptedOut, ColdHeld, CampaignCancelled, InvalidRecipient, SessionNotConnected, AckTimeout, BlockedBySafety, SendFailed, MissingVariable, FrequencyCap, Other } +public sealed class CampaignRecipientFailureReasonConverter() : CampaignStringEnumConverter((CampaignRecipientFailureReason.OptedOut, "opted_out"), (CampaignRecipientFailureReason.ColdHeld, "cold_held"), (CampaignRecipientFailureReason.CampaignCancelled, "campaign_cancelled"), (CampaignRecipientFailureReason.InvalidRecipient, "invalid_recipient"), (CampaignRecipientFailureReason.SessionNotConnected, "session_not_connected"), (CampaignRecipientFailureReason.AckTimeout, "ack_timeout"), (CampaignRecipientFailureReason.BlockedBySafety, "blocked_by_safety"), (CampaignRecipientFailureReason.SendFailed, "send_failed"), (CampaignRecipientFailureReason.MissingVariable, "missing_variable"), (CampaignRecipientFailureReason.FrequencyCap, "frequency_cap"), (CampaignRecipientFailureReason.Other, "other")); +[JsonConverter(typeof(CampaignExportStatusConverter))] public enum CampaignExportStatus { Queued, Sending, Sent, Delivered, Read, Failed, Skipped } +public sealed class CampaignExportStatusConverter() : CampaignStringEnumConverter((CampaignExportStatus.Queued, "queued"), (CampaignExportStatus.Sending, "sending"), (CampaignExportStatus.Sent, "sent"), (CampaignExportStatus.Delivered, "delivered"), (CampaignExportStatus.Read, "read"), (CampaignExportStatus.Failed, "failed"), (CampaignExportStatus.Skipped, "skipped")); + +[JsonConverter(typeof(CampaignVariableValueConverter))] +public readonly struct CampaignVariableValue +{ + internal JsonElement Value + { + get; + } + public JsonValueKind Kind => Value.ValueKind; + public string? StringValue => Kind == JsonValueKind.String ? Value.GetString() : null; + public double? NumberValue => Kind == JsonValueKind.Number ? Value.GetDouble() : null; + public bool? BooleanValue => Kind is JsonValueKind.True or JsonValueKind.False ? Value.GetBoolean() : null; + private CampaignVariableValue(JsonElement value) => Value = value; + public static implicit operator CampaignVariableValue(string value) => value is null ? throw new PolymorfaConfigurationException("A variable string cannot be null.") : new(JsonSerializer.SerializeToElement(CampaignContractValidation.ScalarText(value))); + public static implicit operator CampaignVariableValue(double value) => double.IsFinite(value) ? new(JsonSerializer.SerializeToElement(value)) : throw new PolymorfaConfigurationException("Variables must be finite."); + public static implicit operator CampaignVariableValue(bool value) => new(JsonSerializer.SerializeToElement(value)); + internal static CampaignVariableValue From(JsonElement value) + { + if (value.ValueKind == JsonValueKind.String) + { + _ = value.GetString(); + return new(value.Clone()); + } + if (value.ValueKind is JsonValueKind.True or JsonValueKind.False || value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var number) && double.IsFinite(number)) return new(value.Clone()); + throw new JsonException("Invalid variable scalar."); + } + +} +public sealed class CampaignVariableValueConverter : JsonConverter +{ + public override CampaignVariableValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + return CampaignVariableValue.From(doc.RootElement); + } + public override void Write(Utf8JsonWriter writer, CampaignVariableValue value, JsonSerializerOptions options) + { + if (value.Value.ValueKind == JsonValueKind.Undefined) throw new JsonException("Uninitialized variable."); + value.Value.WriteTo(writer); + } +} +[JsonConverter(typeof(AudienceProfileAttributeValueConverter))] +public readonly struct AudienceProfileAttributeValue +{ + internal JsonElement Value + { + get; + } + public JsonValueKind Kind => Value.ValueKind; + public string? StringValue => Kind == JsonValueKind.String ? Value.GetString() : null; + public double? NumberValue => Kind == JsonValueKind.Number ? Value.GetDouble() : null; + public bool? BooleanValue => Kind is JsonValueKind.True or JsonValueKind.False ? Value.GetBoolean() : null; + private AudienceProfileAttributeValue(JsonElement value) => Value = value; + public static AudienceProfileAttributeValue Null => new(JsonSerializer.SerializeToElement(null)); + public static implicit operator AudienceProfileAttributeValue(string value) => value is null ? throw new PolymorfaConfigurationException("Use explicit Null for attributes.") : new(JsonSerializer.SerializeToElement(CampaignContractValidation.ScalarText(value))); + public static implicit operator AudienceProfileAttributeValue(double value) => double.IsFinite(value) ? new(JsonSerializer.SerializeToElement(value)) : throw new PolymorfaConfigurationException("Attributes must be finite."); + public static implicit operator AudienceProfileAttributeValue(bool value) => new(JsonSerializer.SerializeToElement(value)); + internal static AudienceProfileAttributeValue From(JsonElement value) => value.ValueKind == JsonValueKind.Null ? Null : new(CampaignVariableValue.From(value).Value); +} +public sealed class AudienceProfileAttributeValueConverter : JsonConverter +{ + public override AudienceProfileAttributeValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + return AudienceProfileAttributeValue.From(doc.RootElement); + } + public override void Write(Utf8JsonWriter writer, AudienceProfileAttributeValue value, JsonSerializerOptions options) + { + if (value.Value.ValueKind == JsonValueKind.Undefined) throw new JsonException("Uninitialized attribute."); + value.Value.WriteTo(writer); + } +} + +public sealed record AudienceProfileProvenance([property: JsonRequired] AudienceProfileSource Source, [property: JsonRequired] string Reference); +public sealed record AudienceProfileConsentInput([property: JsonRequired, CampaignLiteral("campaigns")] string Purpose, [property: JsonRequired] bool Allowed, [property: JsonRequired] AudienceProfileSource Source, [property: JsonRequired] string Reference, [property: JsonRequired] long AssertedAt); +public sealed record AudienceProfile([property: JsonRequired] string Id, [property: JsonRequired] string Phone, [property: JsonRequired] long Revision, [property: JsonRequired] IReadOnlyList Tags, [property: JsonRequired] IReadOnlyDictionary Attributes, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] AudienceProfileProvenance? Provenance, [property: JsonRequired] long CreatedAt, [property: JsonRequired] long UpdatedAt); +public sealed record PutAudienceProfileRequest(IReadOnlyList Tags, IReadOnlyDictionary Attributes, long? ExpectedRevision = null, AudienceProfileProvenance? Provenance = null, AudienceProfileConsentInput? Consent = null); +public sealed record AudienceConsentAssertion([property: JsonRequired] string Id, [property: JsonRequired] long ProfileRevision, [property: JsonRequired] long CreatedAt, [property: JsonRequired, CampaignLiteral("campaigns")] string Purpose, [property: JsonRequired] bool Allowed, [property: JsonRequired] AudienceProfileSource Source, [property: JsonRequired] string Reference, [property: JsonRequired] long AssertedAt); +public sealed record AudienceProfilePageInfo([property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? NextCursor, [property: JsonRequired] bool HasMore); +public sealed record AudienceProfilePage([property: JsonRequired] IReadOnlyList Data, [property: JsonRequired] AudienceProfilePageInfo Page); +public sealed record ListAudienceProfilesParameters(string? Cursor = null, int? Limit = null); +public sealed record DeleteAudienceProfileResult([property: JsonRequired] bool Deleted, [property: JsonRequired] string Phone, [property: JsonRequired] long Revision); +public sealed record AudienceProfileImportMapping(string Phone, string Tags, string Attributes, string? ExpectedRevision = null, string? ProvenanceReference = null, string? ConsentAllowed = null, string? ConsentAssertedAt = null, string? ConsentReference = null); +public sealed record CreateAudienceProfileImportRequest(string ReplayKey, string CsvBase64, AudienceProfileImportMapping? Mapping = null); +public sealed record AudienceProfileInvalidSample([property: JsonRequired] long Row, [property: JsonRequired] string Reason); +public sealed record AudienceProfileConflictSample([property: JsonRequired] long Row, [property: JsonRequired, CampaignLiteral("quota", "revision_conflict")] string Reason); +public sealed record AudienceProfileImportJob([property: JsonRequired] string Id, [property: JsonRequired, CampaignLiteral("queued", "completed", "cancelled")] string State, [property: JsonRequired] long TotalValid, [property: JsonRequired] long InvalidCount, [property: JsonRequired] IReadOnlyList InvalidSamples, [property: JsonRequired] IReadOnlyList ConflictSamples, [property: JsonRequired] long ProcessedValid, [property: JsonRequired] long CreatedCount, [property: JsonRequired] long UpdatedCount, [property: JsonRequired] long ConflictCount, [property: JsonRequired] long CreatedAt, [property: JsonRequired] long UpdatedAt, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] long? CompletedAt); + +[JsonConverter(typeof(CampaignReplyFlowSendConverter))] +public abstract record CampaignReplyFlowSend +{ + private protected CampaignReplyFlowSend() { } + public abstract string Kind + { + get; + } +} +public sealed record CampaignReplyText([property: JsonRequired] string Text) : CampaignReplyFlowSend +{ + [CampaignLiteral("text")] public override string Kind => "text"; +} +public sealed record CampaignReplyButton([property: JsonRequired] string Id, [property: JsonRequired] string Text); +public sealed record CampaignReplyButtons([property: JsonRequired] string Body, [property: JsonRequired] IReadOnlyList Buttons, [property: CampaignOptional] string? Footer = null) : CampaignReplyFlowSend +{ + [CampaignLiteral("buttons")] public override string Kind => "buttons"; +} +public sealed record CampaignReplyRow([property: JsonRequired] string Id, [property: JsonRequired] string Title, [property: CampaignOptional] string? Description = null); +public sealed record CampaignReplyList([property: JsonRequired] string Body, [property: JsonRequired] string ButtonText, [property: JsonRequired] IReadOnlyList Rows, [property: CampaignOptional] string? Footer = null) : CampaignReplyFlowSend +{ + [CampaignLiteral("list")] public override string Kind => "list"; +} +public sealed record CampaignReplyForm([property: JsonRequired] string FlowId, [property: JsonRequired] string Body, [property: JsonRequired] string ButtonText, [property: JsonRequired] string Screen, [property: CampaignOptional] string? Footer = null) : CampaignReplyFlowSend +{ + [CampaignLiteral("flow_form")] public override string Kind => "flow_form"; +} +[JsonConverter(typeof(CampaignReplyFlowMatchConverter))] +public abstract record CampaignReplyFlowMatch +{ + private protected CampaignReplyFlowMatch() { } + public abstract string Kind + { + get; + } +} +[JsonConverter(typeof(CampaignReplyChoiceKindConverter))] public enum CampaignReplyChoiceKind { Button, List } +public sealed class CampaignReplyChoiceKindConverter() : CampaignStringEnumConverter((CampaignReplyChoiceKind.Button, "button"), (CampaignReplyChoiceKind.List, "list")); +public sealed record CampaignReplyChoice([property: JsonRequired] CampaignReplyChoiceKind ReplyKind, [property: JsonRequired] string Id) : CampaignReplyFlowMatch +{ + [CampaignLiteral("choice")] public override string Kind => "choice"; +} +public sealed record CampaignReplyKeyword([property: JsonRequired] string Value) : CampaignReplyFlowMatch +{ + [CampaignLiteral("keyword")] public override string Kind => "keyword"; +} +public sealed record CampaignReplySubmission : CampaignReplyFlowMatch +{ + [CampaignLiteral("flow_submission")] public override string Kind => "flow_submission"; +} +public sealed record CampaignReplyFlowEdge([property: JsonRequired] CampaignReplyFlowMatch Match, [property: JsonRequired] string To); +public sealed record CampaignReplyFlowNode([property: JsonRequired] string Id, [property: JsonRequired] IReadOnlyList Edges, [property: CampaignOptional] CampaignReplyFlowSend? Send = null); +public sealed record CampaignReplyFlowDefinition([property: JsonRequired] string EntryNodeId, [property: JsonRequired] IReadOnlyList Nodes); +public sealed record CreateCampaignReplyFlowRequest(string Name, CampaignReplyFlowDefinition Definition, string? FlowKey = null, long? Revision = null); +public sealed record CreatePlatformCampaignReplyFlowRequest(string ProjectId, string Name, CampaignReplyFlowDefinition Definition, string? FlowKey = null, long? Revision = null); +public sealed record CampaignReplyFlow([property: JsonRequired] string Id, [property: JsonRequired] string FlowKey, [property: JsonRequired] long Revision, [property: JsonRequired] string Name, [property: JsonRequired] CampaignReplyFlowDefinition Definition, [property: JsonRequired] long CreatedAt); +public sealed record CampaignProjectParameters(string ProjectId); +public sealed record CampaignTestSendRequest(string SessionId, string To, string? SampleRecipientId = null, IReadOnlyDictionary? Variables = null); +public sealed record PlatformCampaignTestSendRequest(string ProjectId, string SessionId, string To, string? SampleRecipientId = null, IReadOnlyDictionary? Variables = null); +public sealed record CampaignTestSender([property: JsonRequired] string SessionId, [property: JsonRequired] string Phone); +public sealed record CampaignTestSend([property: JsonRequired] string Id, [property: JsonRequired] string CampaignId, [property: JsonRequired, CampaignLiteral("sent")] string Status, [property: JsonRequired] CampaignTestSender From, [property: JsonRequired] string To, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? MessageId, [property: JsonRequired] IReadOnlyDictionary Variables, [property: JsonRequired] long CreatedAt); +public sealed record SetCampaignReplyFlowRequest([property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? ReplyFlowId); +public sealed record SetPlatformCampaignReplyFlowRequest(string ProjectId, [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? ReplyFlowId); +public sealed record CampaignReplyFlowAssignment([property: JsonRequired] string CampaignId, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? ReplyFlowId); +public sealed record CampaignTrackedLinkReport([property: JsonRequired] string Key, [property: JsonRequired] string Url, [property: JsonRequired] long UniqueOpens, [property: JsonRequired] long TotalOpens); +public sealed record CampaignRecipientExportParameters(CampaignExportStatus? Status = null, CampaignRecipientFailureReason? Reason = null, string? Cursor = null, int? Limit = null); +public sealed record PlatformCampaignRecipientExportParameters(string ProjectId, CampaignExportStatus? Status = null, CampaignRecipientFailureReason? Reason = null, string? Cursor = null, int? Limit = null); +public sealed record CampaignRecipientExportPage(string Csv, string? NextCursor, string? Filename); +public sealed record CampaignApprovalRequester([property: JsonRequired] string Type, [property: JsonRequired] string Id); +public sealed record CampaignLaunchApproval([property: JsonRequired] string Id, [property: JsonRequired] string CampaignId, [property: JsonRequired, CampaignLiteral("launch", "resume", "reschedule")] string Action, [property: JsonRequired, CampaignLiteral("pending", "approved", "rejected", "superseded", "expired")] string Status, [property: JsonRequired] IReadOnlyList Reasons, [property: JsonRequired] long RecipientCount, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] long? ScheduledAt, [property: JsonRequired] CampaignApprovalRequester RequestedBy, [property: JsonRequired] long RequestedAt, [property: JsonRequired] long ExpiresAt, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? DecidedBy, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] long? DecidedAt, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? OperationId); +public sealed record CampaignLaunchApprovalSettings([property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] long? RecipientThreshold); +public sealed record CampaignSettings([property: JsonRequired] CampaignLaunchApprovalSettings LaunchApproval); +public sealed record CreateAudienceFromCampaignRequest(string Name, string CampaignId, CampaignRetargetOutcome Outcome, string? ProjectId = null); +public sealed record AudienceFromCampaignResult([property: JsonRequired] string Id, [property: JsonRequired] string Name, [property: JsonRequired, CampaignLiteral("campaign")] string Source, [property: JsonRequired] long RecipientCount, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? FileId, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] IReadOnlyList? Columns, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] IReadOnlyDictionary? SampleRow, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] IReadOnlyDictionary? Mapping, [property: JsonRequired] long CreatedAt, [property: JsonRequired] long UpdatedAt, [property: JsonRequired] string SourceCampaignId, [property: JsonRequired] CampaignRetargetOutcome Outcome, [property: JsonRequired] long MatchedCount, [property: JsonRequired] long OptedOutCount); +public sealed record UpdateStopConfirmationSettingsInput(string TeamId, long Revision, bool Enabled, [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] StopConfirmationLocale? Locale); +public sealed record StopConfirmationSettings([property: JsonRequired] string TeamId, [property: JsonRequired] bool Enabled, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] StopConfirmationLocale? Locale, [property: JsonRequired] long Revision, [property: JsonRequired] int TextRevision, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] string? Acknowledgement, [property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] long? UpdatedAt); +public sealed record StopConfirmationCounts([property: JsonRequired] long Queued, [property: JsonRequired] long Admitted, [property: JsonRequired] long Completed, [property: JsonRequired] long Failed, [property: JsonRequired] long Unknown); +public sealed record StopConfirmationSummary([property: JsonRequired] bool Enabled, [property: JsonRequired] StopConfirmationCounts Counts); +public sealed record SignalCustodyDefault([property: JsonRequired, JsonIgnore(Condition = JsonIgnoreCondition.Never)] SignalCustodyMode? SignalModeDefault, [property: JsonRequired] SignalCustodyMode EffectiveSignalMode, [property: JsonRequired] long Revision); +public sealed record ProjectSignalCustodyUpdate(long ExpectedRevision, SignalCustodyMode SignalModeDefault); +public sealed record CustomerSignalCustodyUpdate(long ExpectedRevision, [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] SignalCustodyMode? SignalModeDefault); diff --git a/packages/dotnet/src/Polymorfa.Sdk/CampaignContractResources.cs b/packages/dotnet/src/Polymorfa.Sdk/CampaignContractResources.cs new file mode 100644 index 00000000..30619a50 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CampaignContractResources.cs @@ -0,0 +1,106 @@ +using System.Text.Json; +namespace Polymorfa.Sdk; + +public sealed class ProjectCampaigns : Resource +{ + private readonly string projectId; + internal ProjectCampaigns(HttpTransport http, string projectId) : base(http) => this.projectId = projectId; + private IReadOnlyList> Owner() => [new("projectId", CampaignContractValidation.Uuid(projectId, "projectId"))]; + private JsonElement Body(JsonElement body) => CampaignContractValidation.Project(body, projectId); + private static string Path(string id) => "/platform/campaigns/" + CampaignContractValidation.Uuid(id, "campaignId"); + public Task>> GetReplyFlowAsync(string replyFlowId, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Get, "/platform/campaign-reply-flows/" + CampaignContractValidation.Uuid(replyFlowId, "replyFlowId"), options: CampaignContractOptions.Select(options), query: Owner()); + public Task>> CreateReplyFlowAsync(CreateCampaignReplyFlowRequest input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Post, "/platform/campaign-reply-flows", Body(CampaignContractValidation.ReplyFlow(input)), CampaignContractOptions.Select(options, true)); + public Task>> SetReplyFlowAsync(string id, SetCampaignReplyFlowRequest input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Put, Path(id) + "/reply-flow", Body(CampaignContractValidation.Assignment(input)), CampaignContractOptions.Select(options, true)); + public Task>> TestSendAsync(string id, CampaignTestSendRequest input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Post, Path(id) + "/test-send", Body(CampaignContractValidation.TestSend(input)), CampaignContractOptions.Select(options, true, true)); + public Task>>> ListLaunchApprovalsAsync(string id, RequestOptions? options = null) => CampaignContractCodec.Request>>(Http, HttpMethod.Get, Path(id) + "/approvals", options: CampaignContractOptions.Select(options), query: Owner()); + public Task>>> LinksAsync(string id, RequestOptions? options = null) => CampaignContractCodec.Request>>(Http, HttpMethod.Get, Path(id) + "/links", options: CampaignContractOptions.Select(options), query: Owner()); + public Task> ExportRecipientsAsync(string id, CampaignRecipientExportParameters? parameters = null, RequestOptions? options = null) => CampaignContractExport.Request(Http, Path(id) + "/recipients/export", CampaignContractValidation.Export(parameters).Concat(Owner()).ToArray(), options); +} +internal static class CampaignContractExport +{ + internal static async Task> Request(HttpTransport http, string path, IReadOnlyList> query, RequestOptions? options) + { + var response = await http.RequestTextAsync(path, "text/csv", CampaignContractOptions.Select(options), query).ConfigureAwait(false); + if (response.Metadata.Status != 200 || !string.Equals(response.Metadata.Headers.GetValueOrDefault("content-type")?.Split(';')[0].Trim(), "text/csv", StringComparison.OrdinalIgnoreCase)) throw new PolymorfaServerException("The API did not return a CSV export.", "invalid_response", response.Metadata); + return new(new(response.Data, response.Metadata.Headers.GetValueOrDefault("polymorfa-next-cursor"), ContentDisposition.ParseFilename(response.Metadata.Headers.GetValueOrDefault("content-disposition"))), response.Metadata); + } +} +public sealed class SignalCustodyResource : Resource +{ + private readonly string projectId; + internal SignalCustodyResource(HttpTransport http, string projectId) : base(http) => this.projectId = projectId; + internal static string Path(string projectId, string? customerId = null) => "/platform/projects/" + CampaignContractValidation.Uuid(projectId, "projectId") + (customerId is null ? "" : "/customers/" + CampaignContractValidation.Uuid(customerId, "customerId")) + "/signal-custody"; + private async Task> Request(HttpMethod method, string? customerId, JsonElement? body, RequestOptions? options) + { + var response = await CampaignContractCodec.Request>(Http, method, Path(projectId, customerId), body, CampaignContractOptions.Select(options, method == HttpMethod.Put)).ConfigureAwait(false); + return new(response.Data.Data, response.Metadata); + } + public Task> RetrieveAsync(RequestOptions? options = null) => Request(HttpMethod.Get, null, null, options); + public Task> UpdateAsync(ProjectSignalCustodyUpdate input, RequestOptions? options = null) => Request(HttpMethod.Put, null, CampaignContractValidation.Custody(input), options); + public Task> RetrieveCustomerAsync(string customerId, RequestOptions? options = null) => Request(HttpMethod.Get, CampaignContractValidation.Uuid(customerId, "customerId"), null, options); + public Task> UpdateCustomerAsync(string customerId, CustomerSignalCustodyUpdate input, RequestOptions? options = null) => Request(HttpMethod.Put, CampaignContractValidation.Uuid(customerId, "customerId"), CampaignContractValidation.Custody(input, true), options); +} +public sealed partial class Projects +{ + public Task>> GetSignalCustodyAsync(string projectId, RequestOptions? options = null) => CampaignContractCodec.Request>(http, HttpMethod.Get, SignalCustodyResource.Path(projectId), options: CampaignContractOptions.Select(options)); + public Task>> UpdateSignalCustodyAsync(string projectId, ProjectSignalCustodyUpdate input, RequestOptions? options = null) => CampaignContractCodec.Request>(http, HttpMethod.Put, SignalCustodyResource.Path(projectId), CampaignContractValidation.Custody(input), CampaignContractOptions.Select(options, true)); +} +public sealed partial class Customers +{ + public Task>> GetSignalCustodyAsync(string customerId, string projectId, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Get, SignalCustodyResource.Path(projectId, CampaignContractValidation.Uuid(customerId, "customerId")), options: CampaignContractOptions.Select(options)); + public Task>> UpdateSignalCustodyAsync(string customerId, string projectId, CustomerSignalCustodyUpdate input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Put, SignalCustodyResource.Path(projectId, CampaignContractValidation.Uuid(customerId, "customerId")), CampaignContractValidation.Custody(input, true), CampaignContractOptions.Select(options, true)); +} +public sealed partial class Audiences +{ + public Task>> CreateFromCampaignAsync(CreateAudienceFromCampaignRequest input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Post, "/platform/audiences/from-campaign", CampaignContractValidation.Retarget(input), CampaignContractOptions.Select(options, true)); +} +public sealed partial class OptOuts +{ + public Task>> GetConfirmationSettingsAsync(RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Get, "/platform/optouts/confirmation-settings", options: CampaignContractOptions.Select(options)); + public Task>> UpdateConfirmationSettingsAsync(UpdateStopConfirmationSettingsInput input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Put, "/platform/optouts/confirmation-settings", CampaignContractValidation.Stop(input), CampaignContractOptions.Select(options, true)); + public Task>> GetConfirmationSummaryAsync(RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Get, "/platform/optouts/confirmation-summary", options: CampaignContractOptions.Select(options)); +} +public sealed partial class PlatformCampaigns +{ + private ProjectCampaigns ContractProject(string projectId) => new(Http, CampaignContractValidation.Uuid(projectId, "projectId")); + public Task>> GetReplyFlowAsync(string id, CampaignProjectParameters parameters, RequestOptions? options = null) => ContractProject(parameters?.ProjectId!).GetReplyFlowAsync(id, options); + public Task>> CreateReplyFlowAsync(CreatePlatformCampaignReplyFlowRequest input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Post, "/platform/campaign-reply-flows", CampaignContractValidation.ReplyFlow(input, true), CampaignContractOptions.Select(options, true)); + public Task>> SetReplyFlowAsync(string id, SetPlatformCampaignReplyFlowRequest input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Put, "/platform/campaigns/" + CampaignContractValidation.Uuid(id, "campaignId") + "/reply-flow", CampaignContractValidation.Assignment(input, true), CampaignContractOptions.Select(options, true)); + public Task>> TestSendAsync(string id, PlatformCampaignTestSendRequest input, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Post, "/platform/campaigns/" + CampaignContractValidation.Uuid(id, "campaignId") + "/test-send", CampaignContractValidation.TestSend(input, true), CampaignContractOptions.Select(options, true, true)); + public Task>>> ListLaunchApprovalsAsync(string id, CampaignProjectParameters parameters, RequestOptions? options = null) => ContractProject(parameters?.ProjectId!).ListLaunchApprovalsAsync(id, options); + public Task>>> LinksAsync(string id, CampaignProjectParameters parameters, RequestOptions? options = null) => ContractProject(parameters?.ProjectId!).LinksAsync(id, options); + public Task> ExportRecipientsAsync(string id, PlatformCampaignRecipientExportParameters parameters, RequestOptions? options = null) + { + if (parameters is null) throw new PolymorfaConfigurationException("A project selector is required."); + return ContractProject(parameters.ProjectId).ExportRecipientsAsync(id, new(parameters.Status, parameters.Reason, parameters.Cursor, parameters.Limit), options); + } + public Task>> GetSettingsAsync(RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Get, "/platform/campaign-settings", options: CampaignContractOptions.Select(options)); +} +public sealed partial class MessagingCampaigns +{ + private string ContractSlug(string slug) + { + Http.Credential.RequireServer(); + CampaignContractValidation.Unicode(slug); + if (string.IsNullOrEmpty(slug)) throw new PolymorfaConfigurationException("A project slug is required."); + return "/messaging/projects/" + E(slug); + } + public Task>> GetReplyFlowAsync(string slug, string id, RequestOptions? options = null) => CampaignContractCodec.Request>(Http, HttpMethod.Get, ContractSlug(slug) + "/campaign-reply-flows/" + CampaignContractValidation.Uuid(id, "replyFlowId"), options: CampaignContractOptions.Select(options)); + public Task>> CreateReplyFlowAsync(string slug, CreateCampaignReplyFlowRequest input, RequestOptions? options = null) + { + var prefix = ContractSlug(slug); + return CampaignContractCodec.Request>(Http, HttpMethod.Post, prefix + "/campaign-reply-flows", CampaignContractValidation.ReplyFlow(input), CampaignContractOptions.Select(options, true)); + } + public Task>> SetReplyFlowAsync(string slug, string id, SetCampaignReplyFlowRequest input, RequestOptions? options = null) + { + var prefix = ContractSlug(slug); + return CampaignContractCodec.Request>(Http, HttpMethod.Put, prefix + "/campaigns/" + CampaignContractValidation.Uuid(id, "campaignId") + "/reply-flow", CampaignContractValidation.Assignment(input), CampaignContractOptions.Select(options, true)); + } + public Task>> TestSendAsync(string slug, string id, CampaignTestSendRequest input, RequestOptions? options = null) + { + var prefix = ContractSlug(slug); + return CampaignContractCodec.Request>(Http, HttpMethod.Post, prefix + "/campaigns/" + CampaignContractValidation.Uuid(id, "campaignId") + "/test-send", CampaignContractValidation.TestSend(input), CampaignContractOptions.Select(options, true, true)); + } + public Task>>> LinksAsync(string slug, string id, RequestOptions? options = null) => CampaignContractCodec.Request>>(Http, HttpMethod.Get, ContractSlug(slug) + "/campaigns/" + CampaignContractValidation.Uuid(id, "campaignId") + "/links", options: CampaignContractOptions.Select(options)); + public Task> ExportRecipientsAsync(string slug, string id, CampaignRecipientExportParameters? parameters = null, RequestOptions? options = null) => CampaignContractExport.Request(Http, ContractSlug(slug) + "/campaigns/" + CampaignContractValidation.Uuid(id, "campaignId") + "/recipients/export", CampaignContractValidation.Export(parameters), options); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CampaignContractValidation.cs b/packages/dotnet/src/Polymorfa.Sdk/CampaignContractValidation.cs new file mode 100644 index 00000000..b79f77bf --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CampaignContractValidation.cs @@ -0,0 +1,458 @@ +using System.Globalization; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +namespace Polymorfa.Sdk; + +internal static class CampaignContractValidation +{ + private static readonly UTF8Encoding Utf8 = new(false, true); + internal static void Invalid(string field) => throw new PolymorfaConfigurationException("Invalid campaign contract field: " + field + "."); + internal static JsonElement Capture(object? input) + { + if (input is null) throw new PolymorfaConfigurationException("A request body is required."); + try + { + return JsonSerializer.SerializeToElement(input, input.GetType(), InputJson); + } + catch (JsonException) + { + throw new PolymorfaConfigurationException("Invalid campaign contract input."); + } + } + private static readonly JsonSerializerOptions InputJson = CreateInputJson(); + private static JsonSerializerOptions CreateInputJson() + { + var options = new JsonSerializerOptions(HttpTransport.Json); + options.Converters.Insert(0, new InputStringConverter()); + return options; + } + // Refuse malformed UTF-16 during the single capture enumeration, before + // System.Text.Json could replace it. Dictionary keys use the same check. + private sealed class InputStringConverter : JsonConverter + { + public override string Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) => reader.GetString()!; + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + Unicode(value); + writer.WriteStringValue(value); + } + public override void WriteAsPropertyName(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + Unicode(value); + writer.WritePropertyName(value); + } + } + internal static void Unicode(string? value) + { + if (value is null) return; + try + { + Utf8.GetByteCount(value); + } + catch (EncoderFallbackException) + { + Invalid("text"); + } + } + internal static string ScalarText(string value) + { + Unicode(value); + return value; + } + internal static string Uuid(string? value, string field) + { + if (value is null || !Regex.IsMatch(value, "\\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\z", RegexOptions.CultureInvariant)) Invalid(field); + return value!; + } + internal static string Phone(string? value) + { + if (value is null || !Regex.IsMatch(value, "\\A\\+[1-9][0-9]{6,14}\\z", RegexOptions.CultureInvariant)) Invalid("phone"); + return value!; + } + internal static string String(JsonElement value, string field, int minimum = 1, int maximum = int.MaxValue, bool bytes = false) + { + if (value.ValueKind != JsonValueKind.String) Invalid(field); + var text = value.GetString()!; + int count; + try + { + count = bytes ? Utf8.GetByteCount(text) : text.Length; + } + catch (EncoderFallbackException) + { + Invalid(field); + throw; + } + if (count < minimum || count > maximum) Invalid(field); + return text; + } + internal static long Integer(JsonElement value, string field, long minimum = 0, long maximum = 9007199254740991) + { + if (value.ValueKind != JsonValueKind.Number || !value.TryGetInt64(out var number) || number < minimum || number > maximum) Invalid(field); + return value.GetInt64(); + } + internal static void Integer(long number, string field, long minimum = 0, long maximum = 9007199254740991) + { + if (number < minimum || number > maximum) Invalid(field); + } + internal static void Fields(JsonElement value, string[] allowed, string[] required, string field = "body") + { + if (value.ValueKind != JsonValueKind.Object) Invalid(field); + if (value.EnumerateObject().Any(p => !allowed.Contains(p.Name)) || required.Any(name => !value.TryGetProperty(name, out _))) Invalid(field); + } + private static bool Has(JsonElement value, string name) => value.TryGetProperty(name, out _); + private static void One(JsonElement value, string[] allowed, string field) + { + if (value.ValueKind != JsonValueKind.String || !allowed.Contains(value.GetString())) Invalid(field); + } + internal static string Trim(string value) => value.Trim('\u0009', '\u000a', '\u000b', '\u000c', '\u000d', '\u0020', '\u00a0', '\u1680', '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', '\u2008', '\u2009', '\u200a', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000', '\ufeff'); + private static bool Control(string value, bool unicode = false) => value.Any(c => unicode ? char.GetUnicodeCategory(c) == UnicodeCategory.Control : c < 32 || c == 127); + private static void Text(JsonElement value, string field, int maximum, bool multiline = false) + { + var text = String(value, field, 1, maximum, true); + if (multiline && Trim(text).Length == 0 || Control(multiline ? text.Replace("\n", "").Replace("\r", "").Replace("\t", "") : text, true)) Invalid(field); + } + private static void Id(JsonElement value, string field) + { + if (!Regex.IsMatch(String(value, field), "\\A[A-Za-z0-9][A-Za-z0-9._:-]{0,63}\\z", RegexOptions.CultureInvariant)) Invalid(field); + } + internal static JsonElement Profile(PutAudienceProfileRequest input) + { + var body = Capture(input); + Fields(body, ["tags", "attributes", "expectedRevision", "provenance", "consent"], ["tags", "attributes"]); + if (Has(body, "expectedRevision")) Integer(body.GetProperty("expectedRevision"), "expectedRevision", 1); + var tags = body.GetProperty("tags"); + if (tags.ValueKind != JsonValueKind.Array || tags.GetArrayLength() > 20) Invalid("tags"); + var seen = new HashSet(StringComparer.Ordinal); + foreach (var raw in tags.EnumerateArray()) + { + var tag = String(raw, "tags", 1, 48, true); + if (Control(tag) || !seen.Add(tag)) Invalid("tags"); + } + var attrs = body.GetProperty("attributes"); + if (attrs.ValueKind != JsonValueKind.Object || attrs.EnumerateObject().Count() > 20 || SemanticBytes(attrs) > 8192) Invalid("attributes"); + foreach (var attr in attrs.EnumerateObject()) + { + if (!Regex.IsMatch(attr.Name, "\\A[A-Za-z_][A-Za-z0-9_]{0,47}\\z", RegexOptions.CultureInvariant)) Invalid("attributes"); + var value = attr.Value; + if (value.ValueKind == JsonValueKind.String) + { + if (String(value, "attributes", 0, 256, true).Contains('\0')) Invalid("attributes"); + } + else if (value.ValueKind == JsonValueKind.Number) + { + if (!value.TryGetDouble(out var number) || !double.IsFinite(number)) Invalid("attributes"); + } + else if (value.ValueKind is not (JsonValueKind.True or JsonValueKind.False or JsonValueKind.Null)) Invalid("attributes"); + } + foreach (var name in new[] { "provenance", "consent" }) + { + if (Has(body, name)) + { + var item = body.GetProperty(name); + var names = name == "consent" ? new[] { "source", "reference", "purpose", "allowed", "assertedAt" } : new[] { "source", "reference" }; + Fields(item, names, names, name); + One(item.GetProperty("source"), ["api", "csv", "form"], name); + if (Control(String(item.GetProperty("reference"), name, 1, 128, true))) Invalid(name); + if (name == "consent") + { + One(item.GetProperty("purpose"), ["campaigns"], name); + if (item.GetProperty("allowed").ValueKind is not (JsonValueKind.True or JsonValueKind.False)) Invalid(name); + Integer(item.GetProperty("assertedAt"), name, 0, 8640000000000000); + } + } + } + return body; + } + internal static IReadOnlyList> Page(ListAudienceProfilesParameters? input) + { + if (input?.Cursor is { } cursor) Uuid(cursor, "cursor"); + if (input?.Limit is { } limit) Integer(limit, "limit", 1, 100); + return Resource.Query(input).ToArray(); + } + internal static JsonElement Import(CreateAudienceProfileImportRequest input) + { + var body = Capture(input); + Fields(body, ["replayKey", "csvBase64", "mapping"], ["replayKey", "csvBase64"]); + if (!Regex.IsMatch(String(body.GetProperty("replayKey"), "replayKey"), "\\A[A-Za-z0-9._:-]{1,128}\\z", RegexOptions.CultureInvariant)) Invalid("replayKey"); + var encoded = String(body.GetProperty("csvBase64"), "csvBase64", 1, 13981016); + if (encoded.Length % 4 != 0 || !Regex.IsMatch(encoded, "\\A[A-Za-z0-9+/]+={0,2}\\z", RegexOptions.CultureInvariant)) Invalid("csvBase64"); + try + { + var csv = Convert.FromBase64String(encoded); + if (csv.Length > 10 * 1024 * 1024 || Convert.ToBase64String(csv) != encoded || Utf8.GetString(csv).Contains('\0')) Invalid("csvBase64"); + } + catch (FormatException) + { + Invalid("csvBase64"); + } + catch (DecoderFallbackException) + { + Invalid("csvBase64"); + } + if (Has(body, "mapping")) + { + var mapping = body.GetProperty("mapping"); + Fields(mapping, ["phone", "tags", "attributes", "expectedRevision", "provenanceReference", "consentAllowed", "consentAssertedAt", "consentReference"], ["phone", "tags", "attributes"], "mapping"); + var headers = new HashSet(StringComparer.Ordinal); + foreach (var property in mapping.EnumerateObject()) + { + var text = String(property.Value, "mapping", 1, 64, true); + if (Trim(text) != text || Control(text, true) || !headers.Add(text)) Invalid("mapping"); + } + } + if (CampaignContractCodec.Encode(body).Length > 14 * 1024 * 1024) Invalid("body"); + return body; + } + internal static JsonElement ReplyFlow(object input, bool project = false) + { + var body = Capture(input); + Fields(body, project ? ["projectId", "name", "definition", "flowKey", "revision"] : ["name", "definition", "flowKey", "revision"], project ? ["projectId", "name", "definition"] : ["name", "definition"]); + String(body.GetProperty("name"), "name", 1, 128, true); + if (project) Uuid(String(body.GetProperty("projectId"), "projectId"), "projectId"); + if (Has(body, "flowKey")) Uuid(String(body.GetProperty("flowKey"), "flowKey"), "flowKey"); + if (Has(body, "revision")) Integer(body.GetProperty("revision"), "revision", 1, 1000000); + var definition = body.GetProperty("definition"); + Fields(definition, ["entryNodeId", "nodes"], ["entryNodeId", "nodes"], "definition"); + Id(definition.GetProperty("entryNodeId"), "entryNodeId"); + var nodes = definition.GetProperty("nodes"); + if (nodes.ValueKind != JsonValueKind.Array || nodes.GetArrayLength() < 2 || nodes.GetArrayLength() > 12) Invalid("nodes"); + var ids = new HashSet(StringComparer.Ordinal); + foreach (var node in nodes.EnumerateArray()) + { + Fields(node, ["id", "send", "edges"], ["id", "edges"], "node"); + Id(node.GetProperty("id"), "node.id"); + if (!ids.Add(node.GetProperty("id").GetString()!)) Invalid("node.id"); + if (Has(node, "send")) Send(node.GetProperty("send")); + var edges = node.GetProperty("edges"); + if (edges.ValueKind != JsonValueKind.Array || edges.GetArrayLength() > 3) Invalid("edges"); + foreach (var edge in edges.EnumerateArray()) + { + Fields(edge, ["match", "to"], ["match", "to"], "edge"); + Id(edge.GetProperty("to"), "edge.to"); + var match = edge.GetProperty("match"); + if (match.ValueKind != JsonValueKind.Object || !Has(match, "kind")) Invalid("match"); + var kind = String(match.GetProperty("kind"), "match.kind"); + switch (kind) + { + case "choice": + Fields(match, ["kind", "replyKind", "id"], ["kind", "replyKind", "id"], "match"); + One(match.GetProperty("replyKind"), ["button", "list"], "replyKind"); + Id(match.GetProperty("id"), "match.id"); + break; + case "keyword": + Fields(match, ["kind", "value"], ["kind", "value"], "match"); + Text(match.GetProperty("value"), "match.value", 64); + break; + case "flow_submission": + Fields(match, ["kind"], ["kind"], "match"); + break; + default: + Invalid("match.kind"); + break; + } + } + } + if (SemanticBytes(nodes, true) > 24 * 1024) Invalid("nodes"); + return body; + } + private static void Send(JsonElement send) + { + if (send.ValueKind != JsonValueKind.Object || !Has(send, "kind")) Invalid("send"); + var kind = String(send.GetProperty("kind"), "send.kind"); + if (kind == "text") + { + Fields(send, ["kind", "text"], ["kind", "text"], "send"); + Text(send.GetProperty("text"), "text", 4096, true); + return; + } + var extras = kind switch { "buttons" => new[] { "buttons" }, "list" => ["buttonText", "rows"], "flow_form" => ["flowId", "buttonText", "screen"], _ => throw new PolymorfaConfigurationException("Unsupported reply-flow send.") }; + Fields(send, new[] { "kind", "body", "footer" }.Concat(extras).ToArray(), new[] { "kind", "body" }.Concat(extras).ToArray(), "send"); + Text(send.GetProperty("body"), "body", 1024, true); + if (Has(send, "footer")) Text(send.GetProperty("footer"), "footer", 60); + if (kind != "buttons") Text(send.GetProperty("buttonText"), "buttonText", 20); + if (kind == "flow_form") + { + Uuid(String(send.GetProperty("flowId"), "flowId"), "flowId"); + if (!Regex.IsMatch(String(send.GetProperty("screen"), "screen"), "\\A[A-Za-z0-9_]{1,64}\\z", RegexOptions.CultureInvariant)) Invalid("screen"); + return; + } + var key = kind == "buttons" ? "buttons" : "rows"; + var choices = send.GetProperty(key); + if (choices.ValueKind != JsonValueKind.Array || choices.GetArrayLength() < 1 || choices.GetArrayLength() > (kind == "buttons" ? 3 : 10)) Invalid(key); + var ids = new HashSet(StringComparer.Ordinal); + foreach (var item in choices.EnumerateArray()) + { + Fields(item, kind == "buttons" ? ["id", "text"] : ["id", "title", "description"], kind == "buttons" ? ["id", "text"] : ["id", "title"], key); + Id(item.GetProperty("id"), "choice.id"); + if (!ids.Add(item.GetProperty("id").GetString()!)) Invalid("choice.id"); + Text(item.GetProperty(kind == "buttons" ? "text" : "title"), key, kind == "buttons" ? 20 : 24); + if (Has(item, "description")) Text(item.GetProperty("description"), "description", 72); + } + } + internal static JsonElement TestSend(object input, bool project = false) + { + var body = Capture(input); + Fields(body, project ? ["projectId", "sessionId", "to", "sampleRecipientId", "variables"] : ["sessionId", "to", "sampleRecipientId", "variables"], project ? ["projectId", "sessionId", "to"] : ["sessionId", "to"]); + Uuid(String(body.GetProperty("sessionId"), "sessionId"), "sessionId"); + String(body.GetProperty("to"), "to", 1, 32); + if (project) Uuid(String(body.GetProperty("projectId"), "projectId"), "projectId"); + if (Has(body, "sampleRecipientId")) Uuid(String(body.GetProperty("sampleRecipientId"), "sampleRecipientId"), "sampleRecipientId"); + if (Has(body, "variables")) + { + var variables = body.GetProperty("variables"); + if (variables.ValueKind != JsonValueKind.Object || variables.EnumerateObject().Count() > 50) Invalid("variables"); + foreach (var variable in variables.EnumerateObject()) + { + if (!Regex.IsMatch(variable.Name, "\\A[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}\\z", RegexOptions.CultureInvariant)) Invalid("variables"); + var value = variable.Value; + string rendered; + if (value.ValueKind == JsonValueKind.String) rendered = value.GetString()!; + else if (value.ValueKind is JsonValueKind.True or JsonValueKind.False) rendered = value.GetBoolean() ? "true" : "false"; + else if (value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var number) && double.IsFinite(number)) rendered = Number(number); + else + { + Invalid("variables"); + throw new InvalidOperationException(); + } + if (rendered.Length > 1024) Invalid("variables"); + } + } + return body; + } + internal static JsonElement Assignment(object input, bool project = false) + { + var body = Capture(input); + Fields(body, project ? ["projectId", "replyFlowId"] : ["replyFlowId"], project ? ["projectId", "replyFlowId"] : ["replyFlowId"]); + if (project) Uuid(String(body.GetProperty("projectId"), "projectId"), "projectId"); + if (body.GetProperty("replyFlowId").ValueKind != JsonValueKind.Null) Uuid(String(body.GetProperty("replyFlowId"), "replyFlowId"), "replyFlowId"); + return body; + } + internal static IReadOnlyList> Export(CampaignRecipientExportParameters? input) + { + var body = Capture(input ?? new()); + Fields(body, ["status", "reason", "cursor", "limit"], []); + if (Has(body, "status")) One(body.GetProperty("status"), ["queued", "sending", "sent", "delivered", "read", "failed", "skipped"], "status"); + if (Has(body, "reason")) One(body.GetProperty("reason"), ["opted_out", "cold_held", "campaign_cancelled", "invalid_recipient", "session_not_connected", "ack_timeout", "blocked_by_safety", "send_failed", "missing_variable", "frequency_cap", "other"], "reason"); + if (Has(body, "cursor")) String(body.GetProperty("cursor"), "cursor", 1, 256); + if (Has(body, "limit")) Integer(body.GetProperty("limit"), "limit", 1, 1000); + if ((Has(body, "status") || Has(body, "reason")) && (Has(body, "cursor") || Has(body, "limit") && body.GetProperty("limit").GetInt32() != 1000)) Invalid("params"); + return Resource.Query(body).ToArray(); + } + internal static JsonElement Retarget(CreateAudienceFromCampaignRequest input) + { + var body = Capture(input); + Fields(body, ["name", "campaignId", "outcome", "projectId"], ["name", "campaignId", "outcome"]); + var name = Trim(String(body.GetProperty("name"), "name")); + if (name.Length < 1 || name.Length > 200) Invalid("name"); + Uuid(String(body.GetProperty("campaignId"), "campaignId"), "campaignId"); + if (Has(body, "projectId")) Uuid(String(body.GetProperty("projectId"), "projectId"), "projectId"); + One(body.GetProperty("outcome"), ["delivered", "not_delivered", "read", "not_read", "replied", "not_replied", "failed"], "outcome"); + return body; + } + internal static JsonElement Stop(UpdateStopConfirmationSettingsInput input) + { + var body = Capture(input); + Fields(body, ["teamId", "revision", "enabled", "locale"], ["teamId", "revision", "enabled", "locale"]); + Uuid(String(body.GetProperty("teamId"), "teamId"), "teamId"); + Integer(body.GetProperty("revision"), "revision", 0, 2147483646); + if (body.GetProperty("enabled").ValueKind is not (JsonValueKind.True or JsonValueKind.False)) Invalid("enabled"); + var locale = body.GetProperty("locale"); + if (locale.ValueKind == JsonValueKind.Null) + { + if (body.GetProperty("enabled").GetBoolean()) Invalid("locale"); + } + else One(locale, ["en", "pt-BR", "es"], "locale"); + return body; + } + internal static JsonElement Custody(object input, bool customer = false) + { + var body = Capture(input); + Fields(body, ["expectedRevision", "signalModeDefault"], ["expectedRevision", "signalModeDefault"]); + Integer(body.GetProperty("expectedRevision"), "expectedRevision"); + if (!(customer && body.GetProperty("signalModeDefault").ValueKind == JsonValueKind.Null)) One(body.GetProperty("signalModeDefault"), ["standard", "zero_knowledge"], "signalModeDefault"); + return body; + } + internal static JsonElement Project(JsonElement body, string projectId) + { + var dictionary = body.EnumerateObject().ToDictionary(p => p.Name, p => p.Value.Clone(), StringComparer.Ordinal); + dictionary.Add("projectId", JsonSerializer.SerializeToElement(Uuid(projectId, "projectId"))); + return JsonSerializer.SerializeToElement(dictionary, HttpTransport.Json); + } + internal static long SemanticBytes(JsonElement value, bool normalizedKeywords = false) => Encoding.UTF8.GetByteCount(Semantic(value, normalizedKeywords)); + private static string Quote(string value) + { + var result = new StringBuilder("\""); + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + switch (c) + { + case '"': + result.Append("\\\""); + break; + case '\\': + result.Append("\\\\"); + break; + case '\b': + result.Append("\\b"); + break; + case '\t': + result.Append("\\t"); + break; + case '\n': + result.Append("\\n"); + break; + case '\f': + result.Append("\\f"); + break; + case '\r': + result.Append("\\r"); + break; + default: + if (c < 32 || char.IsSurrogate(c) && !(char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]))) + { + result.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture)); + } + else + { + result.Append(c); + if (char.IsHighSurrogate(c)) result.Append(value[++i]); + } + break; + } + } + return result.Append('"').ToString(); + } + private static string Semantic(JsonElement value, bool normalized) + { + return value.ValueKind switch { JsonValueKind.Object => "{" + string.Join(',', value.EnumerateObject().Select(p => Quote(p.Name) + ":" + (normalized && p.Name == "value" && value.TryGetProperty("kind", out var kind) && kind.GetString() == "keyword" ? Quote(p.Value.GetString()!.Replace("İ", "i\u0307").ToLowerInvariant()) : Semantic(p.Value, normalized)))) + "}", JsonValueKind.Array => "[" + string.Join(',', value.EnumerateArray().Select(v => Semantic(v, normalized))) + "]", JsonValueKind.String => Quote(value.GetString()!), JsonValueKind.Number => Number(value.GetDouble()), JsonValueKind.True => "true", JsonValueKind.False => "false", JsonValueKind.Null => "null", _ => throw new JsonException() }; + } + private static string Number(double number) + { + if (!double.IsFinite(number)) throw new JsonException(); + if (number == 0) return "0"; + var negative = number < 0; + var absolute = Math.Abs(number); + var repr = absolute.ToString("R", CultureInfo.InvariantCulture).ToLowerInvariant(); + var parts = repr.Split('e'); + var mantissa = parts[0]; + var dot = mantissa.IndexOf('.'); + var position = (dot < 0 ? mantissa.Length : dot) + (parts.Length == 2 ? int.Parse(parts[1], CultureInfo.InvariantCulture) : 0); + var digits = mantissa.Replace(".", ""); + var trimmed = digits.TrimStart('0'); + position -= digits.Length - trimmed.Length; + digits = trimmed.TrimEnd('0'); + string text; + if (absolute >= 1e-6 && absolute < 1e21) text = position <= 0 ? "0." + new string('0', -position) + digits : position >= digits.Length ? digits + new string('0', position - digits.Length) : digits[..position] + "." + digits[position..]; + else + { + text = digits[..1] + (digits.Length > 1 ? "." + digits[1..] : ""); + var exponent = position - 1; + text += "e" + (exponent >= 0 ? "+" : "-") + Math.Abs(exponent).ToString(CultureInfo.InvariantCulture); + } + return (negative ? "-" : "") + text; + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CampaignExistingValidation.cs b/packages/dotnet/src/Polymorfa.Sdk/CampaignExistingValidation.cs new file mode 100644 index 00000000..6aafd06d --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CampaignExistingValidation.cs @@ -0,0 +1,260 @@ +using System.Globalization; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +internal static class CampaignExistingValidation +{ + private static void Invalid(string field) => throw new PolymorfaConfigurationException("Invalid campaign request field: " + field + "."); + private static bool Has(JsonElement value, string name) => value.TryGetProperty(name, out _); + private static bool Match(string value, string expression) => Regex.IsMatch(value, "\\A(?:" + expression + ")\\z", RegexOptions.CultureInvariant); + private static string Text(JsonElement value, string field, int min = 1, int max = int.MaxValue) => CampaignContractValidation.String(value, field, min, max); + private static void Fields(JsonElement value, string[] allowed, string[] required) => CampaignContractValidation.Fields(value, allowed, required); + private static void One(JsonElement value, string field, params string[] allowed) { if (value.ValueKind != JsonValueKind.String || !allowed.Contains(value.GetString())) Invalid(field); } + private static long Integer(JsonElement value, string field, long min, long max) => CampaignContractValidation.Integer(value, field, min, max); + // ECMAScript whitespace and ASCII literal case are intentional. .NET's + // whitespace and IgnoreCase sets also include characters the producer accepts. + internal static bool EcmaSpace(char c) => c is '\u0009' or '\u000a' or '\u000b' or '\u000c' or '\u000d' or '\u0020' or '\u00a0' or '\u1680' or >= '\u2000' and <= '\u200a' or '\u2028' or '\u2029' or '\u202f' or '\u205f' or '\u3000' or '\ufeff'; + private static bool Placeholder(string text) + { + for (var i = 0; i + 1 < text.Length; i++) + { + if (text[i] != '{' || text[i + 1] != '{') continue; + var j = i + 2; while (j < text.Length && EcmaSpace(text[j])) j++; + if (j + 5 <= text.Length && (text[j] is 'l' or 'L') && (text[j + 1] is 'i' or 'I') && (text[j + 2] is 'n' or 'N') && (text[j + 3] is 'k' or 'K') && text[j + 4] == ':') return true; + } + return false; + } + private static IDictionary? Extensions(IDictionary? input, string[] known) + { + if (input is null) return null; + var copy = new Dictionary(StringComparer.Ordinal); + foreach (var p in input) + { + CampaignContractValidation.Unicode(p.Key); + if (known.Contains(p.Key)) Invalid("extensions"); + copy.Add(p.Key, p.Value.Clone()); + } + return copy; + } + private static IReadOnlyList Messages(IReadOnlyList input) => input.Select(message => message with + { + AdditionalFields = Extensions(message.AdditionalFields, ["version", "source", "media", "buttons", "footer", "delayAfterSec", "delayUnit", "sendWindow"]), + Media = message.Media is { } media ? media with { AdditionalFields = Extensions(media.AdditionalFields, ["kind", "url", "filename", "caption"]) } : null, + Buttons = message.Buttons?.Select(button => button with { AdditionalFields = Extensions(button.AdditionalFields, ["kind", "label", "url", "phone"]) }).ToArray() + }).ToArray(); + private static void Unique(JsonElement value) + { + if (value.ValueKind == JsonValueKind.Object) + { + if (value.EnumerateObject().GroupBy(p => p.Name, StringComparer.Ordinal).Any(g => g.Count() != 1)) Invalid("extensions"); + foreach (var p in value.EnumerateObject()) { CampaignContractValidation.Unicode(p.Name); Unique(p.Value); } + } + else if (value.ValueKind == JsonValueKind.Array) foreach (var item in value.EnumerateArray()) Unique(item); + else if (value.ValueKind == JsonValueKind.String) CampaignContractValidation.Unicode(value.GetString()); + } + internal static JsonElement Body(object input, bool platform = false, bool patch = false) + { + if (input is CreatePlatformCampaignRequest { MessagesArray: { } messages } create) input = create with { MessagesArray = Messages(messages) }; + if (input is UpdatePlatformCampaignRequest { MessagesArray: { } updateMessages } updateBody) input = updateBody with { MessagesArray = Messages(updateMessages) }; + if (platform && patch && input is UpdatePlatformCampaignRequest update && update.AdditionalFields is { } extra) + { + var known = new[] { "projectId", "id", "name", "templateId", "recipientListId", "senderConfig", "scheduledAt", "sendWindow", "messageVariations", "variants", "variantStrategy", "followUp", "variableMapping", "trackedLinks", "messagesArray", "frequencyCap", "drip", "segmentId" }; + input = update with { AdditionalFields = Extensions(extra, known) }; + } + var body = Capture(input); + try { Unique(body); Validate(body, platform, patch); } + catch (Exception error) when (error is InvalidOperationException or KeyNotFoundException or JsonException) { Invalid("body"); } + return body; + } + internal static void Validate(JsonElement body, bool platform, bool patch) + { + if (body.ValueKind != JsonValueKind.Object) Invalid("body"); + if (body.EnumerateObject().GroupBy(p => p.Name, StringComparer.Ordinal).Any(g => g.Count() != 1)) Invalid("extensions"); + if (platform && patch && (Has(body, "projectId") || Has(body, "id"))) Invalid("projectId/id"); + if (!platform) + { + var allowed = new[] { "name", "templateId", "recipientListId", "senderConfig", "scheduledAt", "sendWindow", "recipients", "messageVariations", "variants", "variantStrategy", "followUp", "variableMapping", "trackedLinks", "frequencyCap", "drip" }; + Fields(body, allowed, patch ? [] : ["name"]); + if (patch && !body.EnumerateObject().Any()) Invalid("body"); + } + if (platform && !patch) CampaignContractValidation.Uuid(Text(body.GetProperty("projectId"), "projectId"), "projectId"); + if (Has(body, "variableMapping") && !(patch && body.GetProperty("variableMapping").ValueKind == JsonValueKind.Null)) Mapping(body.GetProperty("variableMapping")); + if (Has(body, "trackedLinks")) Links(body.GetProperty("trackedLinks")); + if (patch && Has(body, "templateId") && body.GetProperty("templateId").ValueKind != JsonValueKind.Null) CampaignContractValidation.Uuid(Text(body.GetProperty("templateId"), "templateId"), "templateId"); + if (Has(body, "followUp")) FollowUp(body.GetProperty("followUp")); + // A PATCH can target a stored drip without repeating its settings; the API + // keeps delay-unit, eligibility and stored-state authority. + if (Has(body, "messagesArray")) Composed(body.GetProperty("messagesArray"), patch || Has(body, "drip") && body.GetProperty("drip").ValueKind != JsonValueKind.Null ? 2592000 : 86400); + foreach (var name in new[] { "variants", "messageVariations" }) + if (Has(body, name) && !(body.GetProperty(name).ValueKind == JsonValueKind.Null && (!platform || patch))) Alternatives(body.GetProperty(name), name == "variants", platform); + if (Has(body, "variantStrategy") && !(body.GetProperty("variantStrategy").ValueKind == JsonValueKind.Null && (!platform || patch))) + { + var v = body.GetProperty("variantStrategy"); + Fields(v, ["winnerCriterion", "holdoutPercent", "testSlicePercent", "autoPromote", "testWindowMinutes"], ["winnerCriterion", "holdoutPercent", "autoPromote", "testWindowMinutes"]); + One(v.GetProperty("winnerCriterion"), "winnerCriterion", "delivery", "read", "reply"); + Integer(v.GetProperty("holdoutPercent"), "holdoutPercent", 0, 30); + if (Has(v, "testSlicePercent")) Integer(v.GetProperty("testSlicePercent"), "testSlicePercent", 5, 50); + if (v.GetProperty("autoPromote").ValueKind != JsonValueKind.True) Invalid("autoPromote"); + Integer(v.GetProperty("testWindowMinutes"), "testWindowMinutes", 15, 1440); + } + } + private static void Mapping(JsonElement value) + { + if (value.ValueKind != JsonValueKind.Object || value.EnumerateObject().Count() > 50) Invalid("variableMapping"); + foreach (var p in value.EnumerateObject()) + { + if (!Match(p.Name, "[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}")) Invalid("variableMapping.key"); + Fields(p.Value, ["source", "fallback"], []); + if (Has(p.Value, "source") && !Match(Text(p.Value.GetProperty("source"), "source"), "[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}")) Invalid("source"); + if (Has(p.Value, "fallback") && CampaignContractValidation.Trim(Text(p.Value.GetProperty("fallback"), "fallback", 1, 1024)).Length == 0) Invalid("fallback"); + } + } + private static void Links(JsonElement value) + { + if (value.ValueKind == JsonValueKind.Null) return; + if (value.ValueKind != JsonValueKind.Array || value.GetArrayLength() > 5) Invalid("trackedLinks"); + var keys = new HashSet(StringComparer.Ordinal); + foreach (var entry in value.EnumerateArray()) + { + Fields(entry, ["key", "url"], ["key", "url"]); + var key = Text(entry.GetProperty("key"), "key", 1, 32); + if (!Match(key, "[a-z0-9_]+") || !keys.Add(key)) Invalid("trackedLinks.key"); + Url(Text(entry.GetProperty("url"), "url", 1, 2048)); + } + } + private static void Url(string raw) + { + if (raw.Any(c => EcmaSpace(c) || c < 32 || c == 127)) Invalid("trackedLinks.url"); + // WHATWG special HTTPS URLs treat backslashes as slashes and accept an + // omitted pair of authority slashes. Uri supplies IDNA and path escaping. + if (!raw.StartsWith("https:", StringComparison.OrdinalIgnoreCase)) Invalid("trackedLinks.url"); + var special = raw[6..].TrimStart('/', '\\'); + var end = special.IndexOfAny(['/', '\\', '?', '#']); + var authority = end < 0 ? special : special[..end]; + var remainder = end < 0 ? "" : special[end..]; + if (authority.LastIndexOf('@') is var at && at >= 0) + { + if (authority[..at] is not ("" or ":")) Invalid("trackedLinks.url"); + authority = authority[(at + 1)..]; + } + if (authority.Contains('[') || authority.Contains(']')) Invalid("trackedLinks.url"); + var colon = authority.LastIndexOf(':'); + var rawHost = colon < 0 ? authority : authority[..colon]; + var port = ""; + if (colon >= 0 && authority[(colon + 1)..] is { Length: > 0 } rawPort) + { + if (!int.TryParse(rawPort, NumberStyles.None, CultureInfo.InvariantCulture, out var number) || number > 65535) Invalid("trackedLinks.url"); + if (number != 443) port = ":" + number.ToString(CultureInfo.InvariantCulture); + } + var decoded = Uri.UnescapeDataString(rawHost); + if (decoded.Length == 0 || decoded.Any(c => c < 33 || c is '#' or '/' or ':' or '<' or '>' or '?' or '@' or '[' or '\\' or ']' or '^' or '|' or '%')) Invalid("trackedLinks.url"); + string ascii; + try { ascii = string.Join('.', decoded.Split('.').Select(label => label.All(c => c < 128) ? label.ToLowerInvariant() : new IdnMapping().GetAscii(label).ToLowerInvariant())); } + catch (ArgumentException) { Invalid("trackedLinks.url"); throw; } + var host = ascii.EndsWith('.') ? ascii[..^1] : ascii; + if (!host.Contains('.') || IPAddress.TryParse(host, out _) || Match(host, "[0-9.]+") || Match(host.Split('.')[^1], "[0-9]+") || new[] { ".local", ".internal", ".localhost" }.Any(host.EndsWith)) Invalid("trackedLinks.url"); + var queryAt = remainder.IndexOfAny(['?', '#']); + remainder = queryAt < 0 ? remainder.Replace('\\', '/') : remainder[..queryAt].Replace('\\', '/') + remainder[queryAt..]; + // A placeholder host lets Uri escape and normalize the path while the + // WHATWG host rules above retain accepted empty labels and percent hosts. + if (!Uri.TryCreate("https://example.test" + remainder, UriKind.Absolute, out var uri)) Invalid("trackedLinks.url"); + var normalized = "https://" + ascii + port + uri!.AbsolutePath + uri.Query.Replace("'", "%27", StringComparison.Ordinal) + uri.Fragment; + var length = 0; + foreach (var rune in normalized.EnumerateRunes()) length += rune.IsAscii ? 1 : Encoding.UTF8.GetByteCount(rune.ToString()) * 3; + if (length > 2048) Invalid("trackedLinks.url"); + } + private static void FollowUp(JsonElement value) + { + if (value.ValueKind == JsonValueKind.Null) return; + Fields(value, ["message", "delaySeconds", "expirySeconds"], ["message"]); + var delay = Has(value, "delaySeconds") ? Integer(value.GetProperty("delaySeconds"), "delaySeconds", 60, 604800) : 86400; + var expiry = Has(value, "expirySeconds") ? Integer(value.GetProperty("expirySeconds"), "expirySeconds", 86400, 2592000) : 604800; + if (expiry <= delay) Invalid("expirySeconds"); + var message = value.GetProperty("message"); + if (message.ValueKind != JsonValueKind.Object || !Has(message, "type")) Invalid("message"); + if (message.GetProperty("type").GetString() == "text") + { + Fields(message, ["type", "text"], ["type", "text"]); + if (Placeholder(Text(message.GetProperty("text"), "text", 1, 16384))) Invalid("text"); + } + else + { + Fields(message, ["type", "templateId"], ["type", "templateId"]); + One(message.GetProperty("type"), "type", "template"); + CampaignContractValidation.Uuid(Text(message.GetProperty("templateId"), "templateId"), "templateId"); + } + } + private static void Alternatives(JsonElement value, bool variants, bool platform) + { + if (value.ValueKind != JsonValueKind.Array || value.GetArrayLength() < 2 || value.GetArrayLength() > (variants ? 4 : 5)) Invalid("variants"); + var keys = new HashSet(); long total = 0; + foreach (var item in value.EnumerateArray()) + { + Fields(item, variants ? ["key", "label", "weight", "blueprint"] : ["key", "weight", "blueprint"], variants ? ["key", "label", "weight", "blueprint"] : ["key", "weight", "blueprint"]); + var key = Text(item.GetProperty("key"), "key", 1, 16); + if (!Match(key, "[a-z][a-z0-9_-]{0,15}") || !keys.Add(key)) Invalid("key"); + if (variants) { var label = Text(item.GetProperty("label"), "label"); var bound = platform ? label : CampaignContractValidation.Trim(label); if (bound.Length is < 1 or > 100) Invalid("label"); } + total += Integer(item.GetProperty("weight"), "weight", 1, 99); + var blueprint = item.GetProperty("blueprint"); + if (blueprint.GetProperty("version").ValueKind != JsonValueKind.Number || blueprint.GetProperty("version").GetDouble() != 2 || CampaignContractValidation.Trim(Text(blueprint.GetProperty("source"), "source", 1, 4096)).Length == 0) Invalid("blueprint"); + } + if (total != 100) Invalid("weight"); + } + private static void Composed(JsonElement value, long maxDelay) + { + if (value.ValueKind != JsonValueKind.Array || value.GetArrayLength() > 10) Invalid("messagesArray"); + foreach (var item in value.EnumerateArray()) + { + if (item.ValueKind != JsonValueKind.Object) Invalid("message"); + if (Has(item, "version") && (item.GetProperty("version").ValueKind != JsonValueKind.Number || item.GetProperty("version").GetDouble() != 2)) Invalid("version"); + foreach (var key in new[] { "source", "footer" }) if (Has(item, key)) Text(item.GetProperty(key), key, 0); + if (Has(item, "delayAfterSec")) Integer(item.GetProperty("delayAfterSec"), "delayAfterSec", 0, maxDelay); + if (Has(item, "media")) + { + var media = item.GetProperty("media"); + if (media.ValueKind != JsonValueKind.Object || !Has(media, "kind") || !Has(media, "url")) Invalid("media"); + One(media.GetProperty("kind"), "kind", "image", "video", "audio", "document"); Text(media.GetProperty("url"), "url", 0); + foreach (var key in new[] { "filename", "caption" }) if (Has(media, key)) Text(media.GetProperty(key), key, 0); + } + if (Has(item, "buttons")) + { + var buttons = item.GetProperty("buttons"); if (buttons.ValueKind != JsonValueKind.Array) Invalid("buttons"); + foreach (var button in buttons.EnumerateArray()) + { + if (button.ValueKind != JsonValueKind.Object || !Has(button, "kind") || !Has(button, "label")) Invalid("button"); + One(button.GetProperty("kind"), "kind", "reply", "link", "call"); Text(button.GetProperty("label"), "label", 0); + foreach (var key in new[] { "url", "phone" }) if (Has(button, key)) Text(button.GetProperty(key), key, 0); + } + } + } + } + private static JsonElement Capture(object input) + { + try { return CampaignContractValidation.Capture(input); } + catch (ArgumentException) { throw new PolymorfaConfigurationException("Invalid campaign request input."); } + } + internal static JsonElement Launch(object input, bool platform) + { + var body = Capture(input); + Fields(body, platform ? ["projectId", "scheduledAt", "skipMissingVariables"] : ["scheduledAt", "skipMissingVariables"], platform ? ["projectId"] : []); + if (platform) CampaignContractValidation.Uuid(Text(body.GetProperty("projectId"), "projectId"), "projectId"); + if (Has(body, "scheduledAt") && (body.GetProperty("scheduledAt").ValueKind != JsonValueKind.Number || !double.IsFinite(body.GetProperty("scheduledAt").GetDouble()))) Invalid("scheduledAt"); + if (Has(body, "skipMissingVariables") && body.GetProperty("skipMissingVariables").ValueKind is not (JsonValueKind.True or JsonValueKind.False)) Invalid("skipMissingVariables"); + return body; + } + internal static IReadOnlyList> Recipients(object input, bool platform) + { + var body = Capture(input); + Fields(body, platform ? ["projectId", "status", "reason", "cursor", "limit"] : ["status", "reason", "cursor", "limit"], platform ? ["projectId"] : []); + if (platform) CampaignContractValidation.Uuid(Text(body.GetProperty("projectId"), "projectId"), "projectId"); + if (Has(body, "status")) One(body.GetProperty("status"), "status", "queued", "sending", "sent", "delivered", "read", "failed", "skipped"); + if (Has(body, "reason")) One(body.GetProperty("reason"), "reason", "opted_out", "cold_held", "campaign_cancelled", "invalid_recipient", "session_not_connected", "ack_timeout", "blocked_by_safety", "send_failed", "missing_variable", "frequency_cap", "other"); + if (Has(body, "cursor")) Text(body.GetProperty("cursor"), "cursor", 1, 256); + if (Has(body, "limit")) Integer(body.GetProperty("limit"), "limit", 1, 100); + return Resource.Query(body); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CampaignReplyFlowConverters.cs b/packages/dotnet/src/Polymorfa.Sdk/CampaignReplyFlowConverters.cs new file mode 100644 index 00000000..f2ca86c4 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CampaignReplyFlowConverters.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +namespace Polymorfa.Sdk; + +internal static class CampaignReplyFlowUnions +{ + internal static Type Resolve(Type type, JsonElement value) + { + if (value.ValueKind != JsonValueKind.Object || !value.TryGetProperty("kind", out var kind) || kind.ValueKind != JsonValueKind.String) throw new JsonException("Missing reply-flow kind."); + return (type == typeof(CampaignReplyFlowSend), kind.GetString()) switch + { + (true, "text") => typeof(CampaignReplyText), + (true, "buttons") => typeof(CampaignReplyButtons), + (true, "list") => typeof(CampaignReplyList), + (true, "flow_form") => typeof(CampaignReplyForm), + (false, "choice") => typeof(CampaignReplyChoice), + (false, "keyword") => typeof(CampaignReplyKeyword), + (false, "flow_submission") => typeof(CampaignReplySubmission), + _ => throw new JsonException("Unsupported reply-flow kind.") + }; + } + internal static void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + foreach (var property in value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + var item = property.GetValue(value); + if (item is null && property.GetCustomAttribute() is not null) continue; + writer.WritePropertyName(JsonNamingPolicy.CamelCase.ConvertName(property.Name)); + JsonSerializer.Serialize(writer, item, property.PropertyType, options); + } + writer.WriteEndObject(); + } +} +public sealed class CampaignReplyFlowSendConverter : JsonConverter +{ + public override CampaignReplyFlowSend Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + return (CampaignReplyFlowSend)(doc.RootElement.Deserialize(CampaignReplyFlowUnions.Resolve(typeof(CampaignReplyFlowSend), doc.RootElement), options) ?? throw new JsonException()); + } + public override void Write(Utf8JsonWriter writer, CampaignReplyFlowSend value, JsonSerializerOptions options) => CampaignReplyFlowUnions.Write(writer, value, options); +} +public sealed class CampaignReplyFlowMatchConverter : JsonConverter +{ + public override CampaignReplyFlowMatch Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + return (CampaignReplyFlowMatch)(doc.RootElement.Deserialize(CampaignReplyFlowUnions.Resolve(typeof(CampaignReplyFlowMatch), doc.RootElement), options) ?? throw new JsonException()); + } + public override void Write(Utf8JsonWriter writer, CampaignReplyFlowMatch value, JsonSerializerOptions options) => CampaignReplyFlowUnions.Write(writer, value, options); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CampaignSegmentsDrip.cs b/packages/dotnet/src/Polymorfa.Sdk/CampaignSegmentsDrip.cs new file mode 100644 index 00000000..f525c738 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CampaignSegmentsDrip.cs @@ -0,0 +1,196 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +/// Rolling per-contact limits; null means no additional limit for that window. MaxPer24Hours is 1..50 and cannot exceed MaxPer7Days (1..200). +public sealed record CampaignFrequencyCap([property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] int? MaxPer24Hours, [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] int? MaxPer7Days); +/// The team cap. Revision is 0 and UpdatedAt is null until first saved. +public sealed record CampaignFrequencyCapSettings(string TeamId, [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] int? MaxPer24Hours, [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] int? MaxPer7Days, long Revision, [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] long? UpdatedAt); +/// Complete replacement with the revision from the last read; null limits remove them. +public sealed record UpdateCampaignFrequencyCapRequest([property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] int? MaxPer24Hours, [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] int? MaxPer7Days, long Revision); +public sealed record CampaignFrequencyCapAnalytics(long SkippedRecipientCount); + +[JsonConverter(typeof(CampaignDripEnrollmentConverter))] public enum CampaignDripEnrollment { Audience, Continuous } +public sealed class CampaignDripEnrollmentConverter() : CampaignStringEnumConverter((CampaignDripEnrollment.Audience, "audience"), (CampaignDripEnrollment.Continuous, "continuous")); +[JsonConverter(typeof(CampaignDelayUnitConverter))] public enum CampaignDelayUnit { Minutes, Hours, Days } +public sealed class CampaignDelayUnitConverter() : CampaignStringEnumConverter((CampaignDelayUnit.Minutes, "minutes"), (CampaignDelayUnit.Hours, "hours"), (CampaignDelayUnit.Days, "days")); +/// Stored drip settings. A GET of a tracked link counts as a click, including link previews and scanners. +public sealed record CampaignDrip(CampaignDripEnrollment Enrollment, bool ExitOnReply, bool ExitOnLinkClick); +/// Create settings; the API applies audience, true and false to omitted fields. +public sealed record CampaignDripInput(CampaignDripEnrollment? Enrollment = null, bool? ExitOnReply = null, bool? ExitOnLinkClick = null); +/// Partial settings. Omitted fields keep their stored values; no defaults are applied. +public sealed record CampaignDripPatch(CampaignDripEnrollment? Enrollment = null, bool? ExitOnReply = null, bool? ExitOnLinkClick = null); +public sealed record CampaignDripStepStats(int Step, long Entered, long Waiting, long Sent, long Delivered, long Read, long Replied, long Exited, long Failed); +public sealed record CampaignDripStats(CampaignDripEnrollment Enrollment, long Enrolled, long EnrolledByApi, long Active, long Completed, long Exited, IReadOnlyList Steps); +/// Values that appear in ; arbitrary send failure codes also occur there. +public static class CampaignDripExitReasons { public const string Replied = "replied"; public const string LinkClicked = "link_clicked"; public const string OptedOut = "opted_out"; } +public sealed record CampaignSegmentSnapshot(long Revision, long EvaluatedAt, long MatchedCount); +/// Replaces the campaign weekdays and hours for a later drip step; at most four ranges. +public sealed record CampaignStepWindow(IReadOnlyList Days, IReadOnlyList Hours); +/// A phone already in the drip returns Created = false and never restarts. +public sealed record EnrollCampaignContactRequest(string Phone, IReadOnlyDictionary? Variables = null); +/// Team keys select the owning project explicitly. +public sealed record EnrollPlatformCampaignContactRequest(string ProjectId, string Phone, IReadOnlyDictionary? Variables = null); +/// EnrolledAt is Unix milliseconds. +public sealed record CampaignEnrollment(string CampaignId, string RecipientId, string Phone, bool Created, string Status, double EnrolledAt); + +public sealed partial class PlatformCampaigns +{ + /// Team cap. Team key with campaigns:read. + public Task>> GetFrequencyCapAsync(RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Get, "/platform/campaign-settings/frequency-cap", options: options); + /// Replaces both limits with the last read revision. A stale revision throws ; read again and reconcile. Limits require beta access. Sent once: reconcile a lost response with a fresh read. + public Task>> UpdateFrequencyCapAsync(UpdateCampaignFrequencyCapRequest input, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Put, "/platform/campaign-settings/frequency-cap", ExistingContractCodec.Capture(input), SegmentOptions.Once(options)); + /// Adds a contact to a running or paused continuous drip. Sent once: per-phone server idempotency does not make an ambiguous retry safe. + public Task>> EnrollAsync(string id, EnrollPlatformCampaignContactRequest input, RequestOptions? options = null) + { + CampaignContractValidation.Uuid(input.ProjectId, "projectId"); + return ExistingContractCodec.Request>(Http, HttpMethod.Post, "/platform/campaigns/" + E(id) + "/enrollments", ExistingContractCodec.Capture(input), SegmentOptions.Once(options)); + } +} +public sealed partial class MessagingCampaigns +{ + /// Adds a contact to a running or paused continuous drip; sent once. + public Task>> EnrollAsync(string slug, string id, EnrollCampaignContactRequest input, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, "/messaging/projects/" + E(slug) + "/campaigns/" + E(id) + "/enrollments", ExistingContractCodec.Capture(input), SegmentOptions.Once(options)); +} +internal static class SegmentOptions +{ + internal static RequestOptions Once(RequestOptions? options) => (options ?? new()) with { MaxNetworkRetries = 0 }; +} + +[JsonConverter(typeof(AudienceSegmentMatchConverter))] public enum AudienceSegmentMatch { All, Any } +public sealed class AudienceSegmentMatchConverter() : CampaignStringEnumConverter((AudienceSegmentMatch.All, "all"), (AudienceSegmentMatch.Any, "any")); +[JsonConverter(typeof(AudienceSegmentTextOperatorConverter))] public enum AudienceSegmentTextOperator { Contains, Before, After } +public sealed class AudienceSegmentTextOperatorConverter() : CampaignStringEnumConverter((AudienceSegmentTextOperator.Contains, "contains"), (AudienceSegmentTextOperator.Before, "before"), (AudienceSegmentTextOperator.After, "after")); +[JsonConverter(typeof(AudienceSegmentCompareOperatorConverter))] public enum AudienceSegmentCompareOperator { GreaterThan, LessThan } +public sealed class AudienceSegmentCompareOperatorConverter() : CampaignStringEnumConverter((AudienceSegmentCompareOperator.GreaterThan, "greater_than"), (AudienceSegmentCompareOperator.LessThan, "less_than")); +/// A string, number or boolean condition value, kept as its exact JSON spelling. +public sealed record AudienceSegmentValue +{ + public JsonElement Json { get; } + private AudienceSegmentValue(JsonElement json) => Json = json; + public static AudienceSegmentValue Of(string value) => new(JsonSerializer.SerializeToElement(value)); + public static AudienceSegmentValue Of(long value) => new(JsonSerializer.SerializeToElement(value)); + public static AudienceSegmentValue Of(decimal value) => new(JsonSerializer.SerializeToElement(value)); + public static AudienceSegmentValue Of(double value) => double.IsFinite(value) ? new(JsonSerializer.SerializeToElement(value)) : throw new PolymorfaConfigurationException("Audience segment numbers must be finite."); + public static AudienceSegmentValue Of(bool value) => new(JsonSerializer.SerializeToElement(value)); + internal static AudienceSegmentValue? From(JsonElement json) => json.ValueKind is JsonValueKind.String or JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False ? new(json.Clone()) : null; + public bool Equals(AudienceSegmentValue? other) => other is not null && Json.GetRawText() == other.Json.GetRawText(); + public override int GetHashCode() => Json.GetRawText().GetHashCode(StringComparison.Ordinal); +} +/// One rule condition. Missing, null or wrong-type attributes do not match. Unmodeled server conditions decode as , which requests reject before sending. +[JsonConverter(typeof(AudienceSegmentConditionConverter))] public abstract record AudienceSegmentCondition; +public sealed record AudienceSegmentTagContains(string Value) : AudienceSegmentCondition; +public sealed record AudienceSegmentTagIn(IReadOnlyList Values) : AudienceSegmentCondition; +public sealed record AudienceSegmentTagExists() : AudienceSegmentCondition; +public sealed record AudienceSegmentAttributeEquals(string Name, AudienceSegmentValue Value) : AudienceSegmentCondition; +public sealed record AudienceSegmentAttributeText(string Name, AudienceSegmentTextOperator Operator, string Value) : AudienceSegmentCondition; +/// Up to 50 strings or numbers; booleans are rejected before sending. +public sealed record AudienceSegmentAttributeIn(string Name, IReadOnlyList Values) : AudienceSegmentCondition; +public sealed record AudienceSegmentAttributeCompare(string Name, AudienceSegmentCompareOperator Operator, decimal Value) : AudienceSegmentCondition; +public sealed record AudienceSegmentAttributeExists(string Name) : AudienceSegmentCondition; +public sealed record AudienceSegmentConsentEquals(bool Value) : AudienceSegmentCondition; +/// Outcome is delivered, not_delivered, read, not_read, replied, not_replied or failed. +public sealed record AudienceSegmentCampaignOutcome(string CampaignId, string Outcome) : AudienceSegmentCondition; +public sealed record AudienceSegmentUnknownCondition(JsonElement Raw) : AudienceSegmentCondition; +internal sealed class AudienceSegmentConditionConverter : JsonConverter +{ + public override AudienceSegmentCondition Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + var v = document.RootElement.Clone(); + if (v.ValueKind != JsonValueKind.Object) throw new JsonException("Audience segment condition must be an object."); + var names = v.EnumerateObject().Select(p => p.Name).Order(StringComparer.Ordinal).ToArray(); + bool Keys(params string[] expected) => names.SequenceEqual(expected.Order(StringComparer.Ordinal)); + string? Str(string name) => v.TryGetProperty(name, out var p) && p.ValueKind == JsonValueKind.String ? p.GetString() : null; + var field = Str("field"); var op = Str("operator"); var name = Str("name"); + AudienceSegmentCondition? result = (field, op) switch + { + ("tag", "contains") when Keys("field", "operator", "value") && Str("value") is { } s => new AudienceSegmentTagContains(s), + ("tag", "in") when Keys("field", "operator", "values") && v.GetProperty("values") is { ValueKind: JsonValueKind.Array } a && a.EnumerateArray().All(x => x.ValueKind == JsonValueKind.String) => new AudienceSegmentTagIn(a.EnumerateArray().Select(x => x.GetString()!).ToArray()), + ("tag", "exists") when Keys("field", "operator") => new AudienceSegmentTagExists(), + ("attribute", "equals") when name is not null && Keys("field", "name", "operator", "value") && AudienceSegmentValue.From(v.GetProperty("value")) is { } value => new AudienceSegmentAttributeEquals(name, value), + ("attribute", "contains" or "before" or "after") when name is not null && Keys("field", "name", "operator", "value") && Str("value") is { } s => new AudienceSegmentAttributeText(name, op switch { "contains" => AudienceSegmentTextOperator.Contains, "before" => AudienceSegmentTextOperator.Before, _ => AudienceSegmentTextOperator.After }, s), + ("attribute", "in") when name is not null && Keys("field", "name", "operator", "values") && v.GetProperty("values") is { ValueKind: JsonValueKind.Array } a && a.EnumerateArray().All(x => x.ValueKind is JsonValueKind.String or JsonValueKind.Number) => new AudienceSegmentAttributeIn(name, a.EnumerateArray().Select(x => AudienceSegmentValue.From(x)!).ToArray()), + ("attribute", "greater_than" or "less_than") when name is not null && Keys("field", "name", "operator", "value") && v.GetProperty("value") is { ValueKind: JsonValueKind.Number } n && n.TryGetDecimal(out var number) => new AudienceSegmentAttributeCompare(name, op == "greater_than" ? AudienceSegmentCompareOperator.GreaterThan : AudienceSegmentCompareOperator.LessThan, number), + ("attribute", "exists") when name is not null && Keys("field", "name", "operator") => new AudienceSegmentAttributeExists(name), + ("consent", "equals") when Keys("field", "operator", "value") && v.GetProperty("value").ValueKind is JsonValueKind.True or JsonValueKind.False => new AudienceSegmentConsentEquals(v.GetProperty("value").GetBoolean()), + ("campaign", null) when Keys("campaignId", "field", "outcome") && Str("campaignId") is { } id && Str("outcome") is { } outcome => new AudienceSegmentCampaignOutcome(id, outcome), + _ => null, + }; + return result ?? new AudienceSegmentUnknownCondition(v); + } + public override void Write(Utf8JsonWriter writer, AudienceSegmentCondition value, JsonSerializerOptions options) + { + if (value is AudienceSegmentUnknownCondition unknown) { unknown.Raw.WriteTo(writer); return; } + writer.WriteStartObject(); + switch (value) + { + case AudienceSegmentTagContains c: writer.WriteString("field", "tag"); writer.WriteString("operator", "contains"); writer.WriteString("value", c.Value); break; + case AudienceSegmentTagIn c: writer.WriteString("field", "tag"); writer.WriteString("operator", "in"); writer.WritePropertyName("values"); JsonSerializer.Serialize(writer, c.Values, options); break; + case AudienceSegmentTagExists: writer.WriteString("field", "tag"); writer.WriteString("operator", "exists"); break; + case AudienceSegmentAttributeEquals c: writer.WriteString("field", "attribute"); writer.WriteString("name", c.Name); writer.WriteString("operator", "equals"); writer.WritePropertyName("value"); c.Value.Json.WriteTo(writer); break; + case AudienceSegmentAttributeText c: writer.WriteString("field", "attribute"); writer.WriteString("name", c.Name); writer.WritePropertyName("operator"); JsonSerializer.Serialize(writer, c.Operator, options); writer.WriteString("value", c.Value); break; + case AudienceSegmentAttributeIn c: writer.WriteString("field", "attribute"); writer.WriteString("name", c.Name); writer.WriteString("operator", "in"); writer.WriteStartArray("values"); foreach (var item in c.Values) item.Json.WriteTo(writer); writer.WriteEndArray(); break; + case AudienceSegmentAttributeCompare c: writer.WriteString("field", "attribute"); writer.WriteString("name", c.Name); writer.WritePropertyName("operator"); JsonSerializer.Serialize(writer, c.Operator, options); writer.WriteNumber("value", c.Value); break; + case AudienceSegmentAttributeExists c: writer.WriteString("field", "attribute"); writer.WriteString("name", c.Name); writer.WriteString("operator", "exists"); break; + case AudienceSegmentConsentEquals c: writer.WriteString("field", "consent"); writer.WriteString("operator", "equals"); writer.WriteBoolean("value", c.Value); break; + case AudienceSegmentCampaignOutcome c: writer.WriteString("field", "campaign"); writer.WriteString("campaignId", c.CampaignId); writer.WriteString("outcome", c.Outcome); break; + default: throw new JsonException("Unsupported audience segment condition."); + } + writer.WriteEndObject(); + } +} +/// Up to 20 conditions. Negate inverts the group's result. +public sealed record AudienceSegmentGroup(AudienceSegmentMatch Match, IReadOnlyList Conditions, bool? Negate = null); +/// At most 10 groups, 20 conditions per group, 40 conditions, 5 campaign conditions, 50 values per in and 16 KiB. The API enforces these limits. +public sealed record AudienceSegmentRules(AudienceSegmentMatch Match, IReadOnlyList Groups); +public sealed record AudienceSegment(string Id, string Name, long Revision, AudienceSegmentRules Rules, long CreatedAt, long UpdatedAt); +/// Phone is masked, for example +49*******12. +public sealed record AudienceSegmentSample(string Phone, IReadOnlyList Tags); +public record AudienceSegmentPreview(long MatchedCount, long OptedOutCount, long ConsentWithheldCount, long EligibleCount, IReadOnlyList Sample, long EvaluatedAt); +public sealed record SavedAudienceSegmentPreview(long MatchedCount, long OptedOutCount, long ConsentWithheldCount, long EligibleCount, IReadOnlyList Sample, long EvaluatedAt, long Revision) : AudienceSegmentPreview(MatchedCount, OptedOutCount, ConsentWithheldCount, EligibleCount, Sample, EvaluatedAt); +public sealed record CreateAudienceSegmentRequest(string Name, AudienceSegmentRules Rules); +/// Requires the last observed revision. +public sealed record UpdateAudienceSegmentRequest(long ExpectedRevision) +{ + public string? Name { get; init; } + public AudienceSegmentRules? Rules { get; init; } +} +public sealed record ListAudienceSegmentsParameters(int? Limit = null, string? Cursor = null); +public sealed record AudienceSegmentDeletion(bool Deleted, string Id); + +/// Organization-owned saved audience segments (contracts/campaign-segments-drip.json). Team keys only: campaigns:read for reads, campaigns:manage for writes and unsaved previews. Revision conflicts and bounded evaluation timeouts return 409 state_conflict; re-read, reconcile or simplify the rules. Writes and previews are sent once. +public sealed class AudienceSegments : Resource +{ + internal AudienceSegments(HttpTransport http) : base(http) { } + private static string Path(string id) => string.IsNullOrWhiteSpace(id) ? throw new PolymorfaConfigurationException("A segment ID is required.") : "/platform/audience-segments/" + E(id); + private static void Validate(AudienceSegmentRules rules) + { + foreach (var condition in rules.Groups.SelectMany(g => g.Conditions)) + if (condition is null or AudienceSegmentUnknownCondition || condition is AudienceSegmentAttributeIn { Values: var values } && values.Any(v => v.Json.ValueKind is not (JsonValueKind.String or JsonValueKind.Number))) + throw new PolymorfaConfigurationException("Audience segment requests need supported condition shapes and value types."); + } + public Task>> ListAsync(ListAudienceSegmentsParameters? parameters = null, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Get, "/platform/audience-segments", options: options, query: Query(parameters)); + public Task>> RetrieveAsync(string id, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Get, Path(id), options: options); + public Task>> CreateAsync(CreateAudienceSegmentRequest input, RequestOptions? options = null) + { + Validate(input.Rules); + return ExistingContractCodec.Request>(Http, HttpMethod.Post, "/platform/audience-segments", ExistingContractCodec.Capture(input), SegmentOptions.Once(options)); + } + public Task>> UpdateAsync(string id, UpdateAudienceSegmentRequest input, RequestOptions? options = null) + { + var path = Path(id); + if (input.Rules is { } rules) Validate(rules); + return ExistingContractCodec.Request>(Http, HttpMethod.Patch, path, ExistingContractCodec.Capture(input), SegmentOptions.Once(options)); + } + /// Refused while an unlaunched draft uses the segment. Launched campaigns keep their snapshot and recipients; their segment ID becomes null. + public Task>> DeleteAsync(string id, long expectedRevision, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Delete, Path(id), options: SegmentOptions.Once(options), query: [new("expectedRevision", expectedRevision.ToString(System.Globalization.CultureInfo.InvariantCulture))]); + /// Evaluates the saved segment once; an evaluation timeout is a 409. + public Task>> PreviewAsync(string id, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Get, Path(id) + "/preview", options: SegmentOptions.Once(options)); + public Task>> PreviewRulesAsync(AudienceSegmentRules rules, RequestOptions? options = null) + { + Validate(rules); + return ExistingContractCodec.Request>(Http, HttpMethod.Post, "/platform/audience-segments/preview", ExistingContractCodec.Capture(new { rules }), SegmentOptions.Once(options)); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Campaigns.cs b/packages/dotnet/src/Polymorfa.Sdk/Campaigns.cs new file mode 100644 index 00000000..0ca33f46 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Campaigns.cs @@ -0,0 +1,264 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +public sealed record CampaignBlueprint(int Version, string Source) { [JsonExtensionData] public IDictionary? AdditionalFields { get; init; } } +public sealed record CampaignVariant(string Key, string Label, int Weight, CampaignBlueprint Blueprint); +public sealed record CampaignMessageVariation(string Key, int Weight, CampaignBlueprint Blueprint); +/// A receipt-timed beta follow-up. The API owns enrollment, defaults, bounds and send eligibility. +public sealed record CampaignFollowUp(CampaignFollowUpMessage Message, int? DelaySeconds = null, int? ExpirySeconds = null); +[JsonConverter(typeof(CampaignFollowUpMessageConverter))] public abstract record CampaignFollowUpMessage; +public sealed record CampaignFollowUpText(string Text) : CampaignFollowUpMessage; +public sealed record CampaignFollowUpTemplate(string TemplateId) : CampaignFollowUpMessage; +[JsonConverter(typeof(CampaignFollowUpStatusConverter))] +public enum CampaignFollowUpStatus { WaitingReceipt, Scheduled, Sending, Sent, Cancelled, Expired, Failed, OutcomeUnknown } +[JsonConverter(typeof(CampaignFollowUpAnchorKindConverter))] +public enum CampaignFollowUpAnchorKind { Delivered, Read } +/// Receipt and outcome times are Unix milliseconds; absent receipts are null. +public sealed record CampaignFollowUpSummary(string Id, string NumberId, CampaignFollowUpStatus Status, CampaignFollowUpAnchorKind? AnchorKind, long? FirstDeliveredAt, long? FirstReadAt, long? DueAt, long ExpiresAt, long? SentAt, long? DeliveredAt, long? ReadAt, long? RepliedAt, string? Reason, string? CommandId); +public sealed record CampaignFollowUpReport(long Total, long Waiting, long Scheduled, long Sending, long Sent, long Delivered, long Read, long Replied, long Cancelled, long Expired, long Failed, long Unknown, long? NextDueAt); + +public sealed record CampaignVariantStrategy(string WinnerCriterion, int HoldoutPercent, bool AutoPromote, int TestWindowMinutes, int? TestSlicePercent = null); +[JsonConverter(typeof(CampaignOutcomeConverter))] public abstract record CampaignExperimentOutcome; +public sealed record CampaignPromoted(string WinnerKey) : CampaignExperimentOutcome; +public sealed record CampaignInconclusive(string Reason) : CampaignExperimentOutcome; +public sealed record CampaignExperimentVariantResult(string Key, string Label, int Weight, long Assigned, long Sent, long Delivered, long Read, long Replied, decimal OutcomeRate); +public sealed record CampaignExperimentResults(string Criterion, CampaignExperimentOutcome? Outcome, long HoldoutCount, long ReserveCount, IReadOnlyList Variants); +public sealed record CampaignSendWindowRange(string Start, string End); +public sealed record CampaignSendWindowRequest(IReadOnlyList Days, IReadOnlyList Hours, string? TimeZone = null, bool? RecipientTimeZone = null, string? TimeZoneVariable = null); +public sealed record CampaignSendWindow(string TimeZone, IReadOnlyList Days, IReadOnlyList Hours, bool RecipientTimeZone, string TimeZoneVariable); +[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)] +public sealed record CampaignVariableBinding([property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue Source = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue Fallback = default); +public sealed class CampaignVariableMapping : Dictionary +{ + public CampaignVariableMapping() : base(StringComparer.Ordinal) { } + public CampaignVariableMapping(IDictionary values) : base(values, StringComparer.Ordinal) { } +} +public sealed record CampaignTrackedLink(string Key, string Url); +public sealed record PlatformCampaignMessage +{ + public int? Version { get; init; } + public string? Source { get; init; } + public PlatformCampaignMedia? Media { get; init; } + public IReadOnlyList? Buttons { get; init; } + public string? Footer { get; init; } + /// 0..86400, or 0..2592000 for drip steps, in whole multiples of when set. + public int? DelayAfterSec { get; init; } + public CampaignDelayUnit? DelayUnit { get; init; } + /// Later drip steps only; an explicit null removes a step window. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue SendWindow { get; init; } + [JsonExtensionData] public IDictionary? AdditionalFields { get; init; } +} +public sealed record PlatformCampaignMedia(string Kind, string Url, string? Filename = null, string? Caption = null) +{ + [JsonExtensionData] public IDictionary? AdditionalFields { get; init; } +} +public sealed record PlatformCampaignButton(string Kind, string Label, string? Url = null, string? Phone = null) +{ + [JsonExtensionData] public IDictionary? AdditionalFields { get; init; } +} +public record Campaign +{ + public string Id { get; init; } = ""; + public string Name { get; init; } = ""; + public string Status { get; init; } = ""; + public string? TemplateId { get; init; } + public string? RecipientListId { get; init; } + public long RecipientCount { get; init; } + public long SentCount { get; init; } + public long DeliveredCount { get; init; } + public long ReadCount { get; init; } + public long FailedCount { get; init; } + public long SkippedCount { get; init; } + public long? ScheduledAt { get; init; } + public long? LaunchedAt { get; init; } + public long? CompletedAt { get; init; } + public long CreatedAt { get; init; } + public long UpdatedAt { get; init; } + public CampaignSendWindow? SendWindow { get; init; } + public JsonElement? ComposerBlueprint { get; init; } + public JsonElement? Messages { get; init; } + public JsonElement? AudienceRef { get; init; } + public JsonElement? SenderConfig { get; init; } + public JsonElement? ComplianceConfig { get; init; } + public IReadOnlyList? Variants { get; init; } + public CampaignVariantStrategy? VariantStrategy { get; init; } + public CampaignExperimentOutcome? ExperimentOutcome { get; init; } + public IReadOnlyList? MessageVariations { get; init; } + public CampaignFollowUp? FollowUp { get; init; } + public IReadOnlyList TrackedLinks { get; init; } = null!; + public CampaignVariableMapping? VariableMapping { get; init; } + /// This campaign's own cap; null when only the team cap applies. + public CampaignFrequencyCap? FrequencyCap { get; init; } + /// Null for a campaign that is not a drip. + public CampaignDrip? Drip { get; init; } + public bool? EnrollmentOpen { get; init; } + public string? PauseReason { get; init; } + /// Read-only on the Messaging API; choose segments through the Platform API. + public string? SegmentId { get; init; } + public CampaignSegmentSnapshot? SegmentSnapshot { get; init; } +} +public sealed record CampaignOperation : Campaign { public string OperationId { get; init; } = ""; } +public sealed record CampaignStopOperation : Campaign { public string? OperationId { get; init; } } +public sealed record PlatformCampaign : Campaign { [JsonExtensionData] public IDictionary? AdditionalFields { get; init; } } +public record CampaignAnalytics(string CampaignId, long RecipientCount, long SentCount, long DeliveredCount, long ReadCount, long FailedCount, long SkippedCount, long RespondedCount, decimal ResponseRate, decimal? AverageResponseTimeMs, decimal? MinResponseTimeMs, decimal? MaxResponseTimeMs, CampaignExperimentResults? Experiment, CampaignFollowUpReport FollowUps) +{ + /// Recipients with a message skipped by the contact frequency cap. + public CampaignFrequencyCapAnalytics? FrequencyCap { get; init; } + /// Per-step drip results; null for a campaign that is not a drip. + public CampaignDripStats? Drip { get; init; } +} +public sealed record PlatformCampaignAnalytics(string CampaignId, long RecipientCount, long SentCount, long DeliveredCount, long ReadCount, long FailedCount, long SkippedCount, long RespondedCount, decimal ResponseRate, decimal? AverageResponseTimeMs, decimal? MinResponseTimeMs, decimal? MaxResponseTimeMs, CampaignExperimentResults? Experiment, CampaignFollowUpReport FollowUps) : CampaignAnalytics(CampaignId, RecipientCount, SentCount, DeliveredCount, ReadCount, FailedCount, SkippedCount, RespondedCount, ResponseRate, AverageResponseTimeMs, MinResponseTimeMs, MaxResponseTimeMs, Experiment, FollowUps); +public record CreateCampaignRequest(string Name, string? TemplateId = null, string? RecipientListId = null, IReadOnlyDictionary? SenderConfig = null, long? ScheduledAt = null, CampaignSendWindowRequest? SendWindow = null, IReadOnlyList? Recipients = null, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue> MessageVariations = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue> Variants = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue VariantStrategy = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue FollowUp = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue VariableMapping = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue> TrackedLinks = default) +{ + /// Beta campaign cap stricter than the team cap; null uses the team cap. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue FrequencyCap { get; init; } + /// Beta drip; null creates an ordinary campaign. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue Drip { get; init; } +} +public sealed record CreatePlatformCampaignRequest(string ProjectId, string Name, string? TemplateId = null, string? RecipientListId = null, IReadOnlyDictionary? SenderConfig = null, long? ScheduledAt = null, CampaignSendWindowRequest? SendWindow = null, IReadOnlyList? Recipients = null, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue> MessageVariations = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue> Variants = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue VariantStrategy = default, long? RecipientCount = null, JsonElement? ComposerBlueprint = null, IReadOnlyList? MessagesArray = null, JsonElement? AudienceRef = null, JsonElement? ComplianceConfig = null, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue FollowUp = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue VariableMapping = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue> TrackedLinks = default) +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue FrequencyCap { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue Drip { get; init; } + /// Beta saved segment evaluated at launch. Create permits omission, not null. + public string? SegmentId { get; init; } +} +public record UpdateCampaignRequest +{ + public string? Name { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue TemplateId { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue VariableMapping { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue> TrackedLinks { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue RecipientListId { get; init; } + public IReadOnlyDictionary? SenderConfig { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue ScheduledAt { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue SendWindow { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue> MessageVariations { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue> Variants { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue VariantStrategy { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue FollowUp { get; init; } + /// Replaces the campaign cap; null removes it. Draft or paused only. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue FrequencyCap { get; init; } + /// Changes only the fields set; null makes the draft an ordinary campaign. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue Drip { get; init; } +} +public sealed record UpdatePlatformCampaignRequest : UpdateCampaignRequest +{ + public IReadOnlyList? MessagesArray { get; init; } + /// Unlaunched drafts only; null clears the saved segment. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public PatchValue SegmentId { get; init; } + [JsonExtensionData] public IDictionary? AdditionalFields { get; init; } +} +public sealed record LaunchCampaignRequest(double? ScheduledAt = null, bool? SkipMissingVariables = null); +public sealed record LaunchPlatformCampaignRequest(string ProjectId, double? ScheduledAt = null, bool? SkipMissingVariables = null); +public sealed record PlatformCampaignLaunchResult(string Id, string Status, string OperationId) +{ + [JsonExtensionData] public IDictionary? AdditionalFields { get; init; } +} +public sealed record RescheduleCampaignRequest([property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] long? ScheduledAt); +public sealed record ReschedulePlatformCampaignRequest(string ProjectId, [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] long? ScheduledAt); +public sealed record RequeueCampaignRequest(bool? IncludeSkippedError = null); +public sealed record CampaignRequeueResult(long Requeued); +public sealed record ListCampaignRecipientsParameters(string? Status = null, string? Cursor = null, int? Limit = null, CampaignRecipientFailureReason? Reason = null); +public sealed record CampaignRecipient(string Id, string Phone, IReadOnlyDictionary Variables, string? VariantKey, string Status, long Attempts, string? LastError, string? ExternalMessageId, long QueuedAt, long? SentAt, long? DeliveredAt, long? ReadAt, long? FailedAt, long? RespondedAt, CampaignRecipientFailureReason? FailureReason, long MessagesSent, double? NextMessageAt, string? SequenceError, CampaignFollowUpSummary? FollowUp); +public sealed record CampaignRecipientPage(string? NextCursor, bool HasMore); +public sealed record CampaignRecipientsResponse(bool Success, IReadOnlyList Data, CampaignRecipientPage Page); +public sealed record AddCampaignRecipientsRequest(IReadOnlyList Recipients); +public sealed record AddPlatformCampaignRecipientsRequest(string ProjectId, IReadOnlyList Recipients); +public sealed record AddCampaignRecipientsResult(string CampaignId, long Added, long RecipientCount, long DuplicateCount, long InvalidCount, IReadOnlyList InvalidRows); +public sealed record ListCampaignsParameters(string ProjectId, string? ProjectSlug = null); +public sealed record PlatformCampaignParameters(string ProjectId); +public sealed record ListPlatformCampaignRecipientsParameters(string ProjectId, string? Status = null, string? Cursor = null, int? Limit = null, CampaignRecipientFailureReason? Reason = null); +public sealed record CampaignConversionValue(long AmountMinor, string Currency); +public sealed record RecordCampaignConversionRequest(string ProjectId, string RecipientId, string EventId, string EventType, string OccurredAt, CampaignConversionValue? Value = null); +public sealed record CampaignConversionAttribution(string Outcome, string? TouchAt, int WindowDays); +public sealed record CampaignConversion(string Id, string CampaignId, string? RecipientId, string EventType, string OccurredAt, CampaignConversionValue? Value, string Evidence, CampaignConversionAttribution Attribution, string RecordedAt, bool Replayed); +public sealed record CampaignConversionCurrencyTotal(string Currency, string Evidence, long AttributedConversions, string AttributedAmountMinor, long UnattributedConversions, string UnattributedAmountMinor); +public sealed record CampaignConversionModel(string Touch, int WindowDays, string Correlation); +public sealed record CampaignConversionCounts(long Total, long Attributed, long OutsideWindow, long NotSent, long OptedOut); +public sealed record CampaignConversionReport(string CampaignId, CampaignConversionModel Model, long SentCount, CampaignConversionCounts Conversions, long ConvertedRecipients, decimal ConversionRate, IReadOnlyList Values); +internal static class CampaignOptions +{ + internal static RequestOptions Idempotent(RequestOptions? options) => (options ?? new()).WithIdempotency(); + internal static RequestOptions Append(RequestOptions? options) => options is { MaxNetworkRetries: not null, IdempotencyKey: not null } ? options : (options ?? new()) with { MaxNetworkRetries = 0 }; +} +public sealed partial class MessagingCampaigns : Resource +{ + internal MessagingCampaigns(HttpTransport http) : base(http) { } + private static string Path(string slug) => "/messaging/projects/" + E(slug) + "/campaigns"; + private static string Path(string slug, string id) => Path(slug) + "/" + E(id); + public Task>>> ListAsync(string slug, RequestOptions? options = null) => ExistingContractCodec.Request>>(Http, HttpMethod.Get, Path(slug), options: options); + public Task>> CreateAsync(string slug, CreateCampaignRequest input, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, Path(slug), CampaignExistingValidation.Body(input), CampaignOptions.Idempotent(options)); + public Task>> RetrieveAsync(string slug, string id, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Get, Path(slug, id), options: options); + public Task>> UpdateAsync(string slug, string id, UpdateCampaignRequest input, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Patch, Path(slug, id), CampaignExistingValidation.Body(input, patch: true), CampaignOptions.Append(options)); + public Task>> AnalyticsAsync(string slug, string id, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Get, Path(slug, id) + "/analytics", options: options); + public Task>> LaunchAsync(string slug, string id, LaunchCampaignRequest? input = null, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, Path(slug, id) + "/launch", CampaignExistingValidation.Launch(input ?? new(), false), CampaignOptions.Idempotent(options)); + public Task>> RescheduleAsync(string slug, string id, RescheduleCampaignRequest input, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, Path(slug, id) + "/reschedule", ExistingContractCodec.Capture(input), CampaignOptions.Idempotent(options)); + public Task>> PauseAsync(string slug, string id, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, Path(slug, id) + "/pause", null, CampaignOptions.Idempotent(options)); + public Task>> ResumeAsync(string slug, string id, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, Path(slug, id) + "/resume", null, CampaignOptions.Idempotent(options)); + public Task>> StopAsync(string slug, string id, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, Path(slug, id) + "/stop", null, CampaignOptions.Idempotent(options)); + public Task> ListRecipientsAsync(string slug, string id, ListCampaignRecipientsParameters? parameters = null, RequestOptions? options = null) => ExistingContractCodec.Request(Http, HttpMethod.Get, Path(slug, id) + "/recipients", options: options, query: CampaignExistingValidation.Recipients(parameters ?? new(), false)); + public Task>> AddRecipientsAsync(string slug, string id, AddCampaignRecipientsRequest input, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, Path(slug, id) + "/recipients", ExistingContractCodec.Capture(input), CampaignOptions.Append(options)); + public Task>> RequeueAsync(string slug, string id, RequeueCampaignRequest? input = null, RequestOptions? options = null) => Post>(Path(slug, id) + "/requeue", input ?? new(), options); +} +public sealed partial class PlatformCampaigns : Resource +{ + internal PlatformCampaigns(HttpTransport http) : base(http) { } + private static string Path(string id) => "/platform/campaigns/" + E(id); + public Task>>> ListAsync(ListCampaignsParameters parameters, RequestOptions? options = null) => ExistingContractCodec.Request>>(Http, HttpMethod.Get, "/platform/campaigns", options: options, query: Query(parameters)); + public Task>> CreateAsync(CreatePlatformCampaignRequest input, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, "/platform/campaigns", CampaignExistingValidation.Body(input, true), (options ?? new()) with { IdempotencyKey = null, MaxNetworkRetries = 0 }); + public Task>> RetrieveAsync(string id, PlatformCampaignParameters parameters, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Get, Path(id), options: options, query: Query(parameters), nullableData: true); + public Task>> UpdateAsync(string id, UpdatePlatformCampaignRequest? input, PlatformCampaignParameters parameters, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Patch, Path(id), input is null ? null : CampaignExistingValidation.Body(input, true, true), options, Query(parameters)); + public Task>>> DeleteAsync(string id, PlatformCampaignParameters parameters, RequestOptions? options = null) => Http.RequestAsync>>(HttpMethod.Delete, Path(id), null, options, Query(parameters)); + private Task>>> Action(string id, string action, IReadOnlyDictionary? input, RequestOptions? options, bool idempotent) => Post>>(Path(id) + "/" + action, input, idempotent ? CampaignOptions.Idempotent(options) : options); + public Task>> LaunchAsync(string id, LaunchPlatformCampaignRequest input, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, Path(id) + "/launch", CampaignExistingValidation.Launch(input, true), CampaignOptions.Idempotent(options)); + public Task>>> PauseAsync(string id, IReadOnlyDictionary? input = null, RequestOptions? options = null) => Action(id, "pause", input, options, true); + public Task>>> ResumeAsync(string id, IReadOnlyDictionary? input = null, RequestOptions? options = null) => Action(id, "resume", input, options, true); + public Task>>> StopAsync(string id, IReadOnlyDictionary? input = null, RequestOptions? options = null) => Action(id, "stop", input, options, true); + public Task>>> ArchiveAsync(string id, IReadOnlyDictionary? input = null, RequestOptions? options = null) => Action(id, "archive", input, options, false); + public Task>>> DuplicateAsync(string id, IReadOnlyDictionary? input = null, RequestOptions? options = null) => Action(id, "duplicate", input, options, false); + public Task>>> RequeueAsync(string id, IReadOnlyDictionary? input = null, RequestOptions? options = null) => Action(id, "requeue", input, options, false); + public Task>>> RescheduleAsync(string id, ReschedulePlatformCampaignRequest input, RequestOptions? options = null) => Post>>(Path(id) + "/reschedule", input, CampaignOptions.Idempotent(options)); + public Task>> AnalyticsAsync(string id, PlatformCampaignParameters parameters, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Get, Path(id) + "/analytics", options: options, query: Query(parameters)); + public Task>>> EventsAsync(string id, PlatformCampaignParameters parameters, RequestOptions? options = null) => Get>>(Path(id) + "/events", options, parameters); + public Task>> RecipientsAsync(string id, ListPlatformCampaignRecipientsParameters parameters, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Get, Path(id) + "/recipients", options: options, query: CampaignExistingValidation.Recipients(parameters, true), platformRecipients: true); + public Task>> AddRecipientsAsync(string id, AddPlatformCampaignRecipientsRequest input, RequestOptions? options = null) => ExistingContractCodec.Request>(Http, HttpMethod.Post, Path(id) + "/recipients", ExistingContractCodec.Capture(input), CampaignOptions.Append(options)); + public Task>> RecordConversionAsync(string id, RecordCampaignConversionRequest input, RequestOptions? options = null) => Post>(Path(id) + "/conversions", input, options); + public Task>> ConversionsAsync(string id, PlatformCampaignParameters parameters, RequestOptions? options = null) => Get>(Path(id) + "/conversions", options, parameters); +} +internal sealed class CampaignOutcomeConverter : JsonConverter +{ + public override CampaignExperimentOutcome Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { using var doc = JsonDocument.ParseValue(ref reader); var json = doc.RootElement; return json.GetProperty("state").GetString() switch { "promoted" => new CampaignPromoted(json.GetProperty("winnerKey").GetString()!), "inconclusive" => new CampaignInconclusive(json.GetProperty("reason").GetString()!), _ => throw new JsonException("Invalid experiment outcome.") }; } + public override void Write(Utf8JsonWriter writer, CampaignExperimentOutcome value, JsonSerializerOptions options) { writer.WriteStartObject(); if (value is CampaignPromoted p) { writer.WriteString("state", "promoted"); writer.WriteString("winnerKey", p.WinnerKey); } else if (value is CampaignInconclusive i) { writer.WriteString("state", "inconclusive"); writer.WriteString("reason", i.Reason); } else throw new JsonException("Invalid experiment outcome."); writer.WriteEndObject(); } +} + +internal sealed class CampaignFollowUpStatusConverter() : JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower, allowIntegerValues: false); +internal sealed class CampaignFollowUpAnchorKindConverter() : JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower, allowIntegerValues: false); +internal sealed class CampaignFollowUpMessageConverter : JsonConverter +{ + public override CampaignFollowUpMessage Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + var value = document.RootElement; + if (value.ValueKind != JsonValueKind.Object || !value.TryGetProperty("type", out var type) || type.ValueKind != JsonValueKind.String) throw new JsonException("Follow-up message type is required."); + return type.GetString() switch + { + "text" => new CampaignFollowUpText(RequiredString(value, "text")), + "template" => new CampaignFollowUpTemplate(RequiredString(value, "templateId")), + _ => throw new JsonException("Invalid follow-up message type.") + }; + } + private static string RequiredString(JsonElement value, string field) => value.TryGetProperty(field, out var result) && result.ValueKind == JsonValueKind.String ? result.GetString()! : throw new JsonException($"Follow-up {field} is required."); + public override void Write(Utf8JsonWriter writer, CampaignFollowUpMessage value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + switch (value) + { + case CampaignFollowUpText text: writer.WriteString("type", "text"); writer.WriteString("text", text.Text); break; + case CampaignFollowUpTemplate template: writer.WriteString("type", "template"); writer.WriteString("templateId", template.TemplateId); break; + default: throw new JsonException("Invalid follow-up message type."); + } + writer.WriteEndObject(); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Channels.cs b/packages/dotnet/src/Polymorfa.Sdk/Channels.cs new file mode 100644 index 00000000..3b7161d7 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Channels.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +public sealed record Channel(string? Id = null, string? Name = null, string? Description = null, string? ProfileUrl = null, long? Followers = null, bool? Muted = null, bool? Preview = null); +public sealed record CreateChannelRequest(string Name, string? Description = null, string? Picture = null); +public sealed record ChannelMessage(long Position, string Id, [property: JsonPropertyName("whatsapp_ids")] WhatsAppMessageIds WhatsAppIds, ConversationReference Conversation, string Type, string Timestamp, long Views, IReadOnlyDictionary ReactionCounts, string? Text = null, [property: JsonPropertyName("whatsapp_id")] string? WhatsAppId = null); +public sealed record ChannelMessagesParameters(int? Count = null, long? Before = null); +public sealed record ChannelMessageUpdatesParameters(int? Count = null, long? Since = null, long? After = null); +public sealed record ChannelReactionRequest(string Reaction); +public sealed record ChannelLiveUpdates(int DurationSeconds); +public sealed record CommandResponse(bool Success, CommandResult? Data = null, string? Message = null); +public sealed class Channels : Resource +{ + internal Channels(HttpTransport http) : base(http) { } + private static string Path(string session) => $"/messaging/{E(session)}/channels"; + private static string Path(string session, string channel) => Path(session) + "/" + E(channel); + public Task>>> ListAsync(string session, RequestOptions? options = null) => Get>>(Path(session), options); + public Task>>> CreateAsync(string session, CreateChannelRequest body, RequestOptions? options = null) => Post>>(Path(session), body, options); + public Task>> RetrieveAsync(string session, string channelId, RequestOptions? options = null) => Get>(Path(session, channelId), options); + public Task>>> DeleteAsync(string session, string channelId, RequestOptions? options = null) => Delete>>(Path(session, channelId), options); + public Task>>> ListMessagesAsync(string session, string channelId, ChannelMessagesParameters? parameters = null, RequestOptions? options = null) => Get>>(Path(session, channelId) + "/messages", options, parameters); + public Task>>> ListMessageUpdatesAsync(string session, string channelId, ChannelMessageUpdatesParameters? parameters = null, RequestOptions? options = null) => Get>>(Path(session, channelId) + "/message-updates", options, parameters); + public Task>> MarkMessageViewedAsync(string session, string channelId, string messageId, RequestOptions? options = null) => Post>(Path(session, channelId) + "/messages/" + E(messageId) + "/viewed", null, options); + public Task>> ReactToMessageAsync(string session, string channelId, string messageId, ChannelReactionRequest body, RequestOptions? options = null) => Post>(Path(session, channelId) + "/messages/" + E(messageId) + "/reaction", body, (options ?? new()).WithIdempotency()); + public Task>>> SubscribeToLiveUpdatesAsync(string session, string channelId, RequestOptions? options = null) => Post>>(Path(session, channelId) + "/live-updates", null, options); + public Task>> FollowAsync(string session, string channelId, RequestOptions? options = null) => Post>(Path(session, channelId) + "/follow", null, options); + public Task>> UnfollowAsync(string session, string channelId, RequestOptions? options = null) => Post>(Path(session, channelId) + "/unfollow", null, options); + public Task>> MuteAsync(string session, string channelId, RequestOptions? options = null) => Post>(Path(session, channelId) + "/mute", null, options); + public Task>> UnmuteAsync(string session, string channelId, RequestOptions? options = null) => Post>(Path(session, channelId) + "/unmute", null, options); +} +public sealed record BusinessQuickReplyMutation(string Shortcut, string Message, IReadOnlyList? Keywords = null, int? Count = null); +public record BusinessQuickReply(string Id, string Shortcut, string Message, IReadOnlyList? Keywords = null, int? Count = null); +public sealed record BusinessQuickReplyObserved(string Id, string Shortcut, string Message, IReadOnlyList AssociatedLabelIds, string ObservedAt, IReadOnlyList? Keywords = null, int? Count = null) : BusinessQuickReply(Id, Shortcut, Message, Keywords, Count); +public sealed record BusinessQuickReplyCollection(string Policy, string Status, IReadOnlyList QuickReplies, string? UnknownReason = null, string? ObservedAt = null); +public sealed record DeletedBusinessQuickReply(string Id, string Status); +public sealed class QuickReplies : Resource +{ + internal QuickReplies(HttpTransport http) : base(http) { } + private static string Path(string session) => $"/messaging/{E(session)}/business/quick-replies"; + public Task>> ListAsync(string session, RequestOptions? options = null) => Get>(Path(session), options); + public Task>> CreateAsync(string session, BusinessQuickReplyMutation body, RequestOptions? options = null) => Post>(Path(session), body, options); + public Task>> ReplaceAsync(string session, string quickReplyId, BusinessQuickReplyMutation body, RequestOptions? options = null) => Put>(Path(session) + "/" + E(quickReplyId), body, options); + public Task>> DeleteAsync(string session, string quickReplyId, RequestOptions? options = null) => Delete>(Path(session) + "/" + E(quickReplyId), options); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CloudGraph.cs b/packages/dotnet/src/Polymorfa.Sdk/CloudGraph.cs new file mode 100644 index 00000000..80940119 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CloudGraph.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +public sealed record CloudProductCatalog(string Id, string? Name = null); +public sealed record CloudCatalogProduct(string Id, [property: JsonPropertyName("retailer_id")] string? RetailerId = null, string? Name = null, string? Availability = null); +public sealed record CloudGraphCursors(string? Before = null, string? After = null); +public sealed record CloudGraphPaging(CloudGraphCursors Cursors); +public sealed record CloudCatalogList(IReadOnlyList Data, CloudGraphPaging? Paging = null); +public sealed record CloudCatalogProductList(IReadOnlyList Data, CloudGraphPaging? Paging = null); +public sealed record CloudMarketingStatus(string Id, [property: JsonPropertyName("marketing_messages_onboarding_status")] string? MarketingMessagesOnboardingStatus = null, [property: JsonPropertyName("owner_business_info")] CloudMarketingOwnerBusiness? OwnerBusinessInfo = null, [property: JsonPropertyName("disable_marketing_messages_on_cloud_api")] bool? DisableMarketingMessagesOnCloudApi = null); +public sealed record FlowEncryptionKey([property: JsonPropertyName("business_public_key")] string BusinessPublicKey, [property: JsonPropertyName("business_public_key_signature_status")] string BusinessPublicKeySignatureStatus); +public sealed record FlowEncryptionKeyList(IReadOnlyList Data); +public sealed record RegisterFlowEncryptionKeyRequest([property: JsonPropertyName("business_public_key")] string BusinessPublicKey); +public sealed record CloudCatalogParameters(string Version, int? Limit = null, string? After = null); +public sealed record CloudMarketingTerms(string? Status = null, string? Time = null); +public sealed record CloudMarketingOwnerBusiness([property: JsonPropertyName("marketing_messages_onboarding_status")] CloudMarketingTerms? MarketingMessagesOnboardingStatus = null); +public sealed class CloudGraph : Resource +{ + public Task>> GetMarketingMessagesStatusAsync(string session, RequestOptions? options = null) + { + Http.Credential.RequireServer(); + if (string.IsNullOrWhiteSpace(session) || session.Length > 128) throw new PolymorfaValidationException("Provide a Number ID of 1 to 128 characters.", "invalid_parameter"); + return Get>("/messaging/" + E(session) + "/marketing-messages/status", options); + } + internal CloudGraph(HttpTransport http) : base(http) { } + private string Path(string id, string version) { Http.Credential.RequireServer(); if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(version)) throw new PolymorfaConfigurationException("Provide an identifier and Graph version."); return "/graph/whatsapp/" + E(version) + "/" + E(id); } + public Task> ListCatalogsAsync(string waba, CloudCatalogParameters parameters, RequestOptions? options = null) => Get(Path(waba, parameters.Version) + "/product_catalogs", options, new { parameters.Limit, parameters.After }); + public Task> ListProductsAsync(string waba, string catalog, CloudCatalogParameters parameters, RequestOptions? options = null) { if (!System.Text.RegularExpressions.Regex.IsMatch(catalog, "^[0-9]+$")) throw new PolymorfaConfigurationException("Provide a numeric catalog ID."); return Get(Path(waba, parameters.Version) + "/product_catalogs/" + E(catalog) + "/products", options, new { parameters.Limit, parameters.After }); } + public Task> MarketingStatusAsync(string waba, string version, RequestOptions? options = null) => Get(Path(waba, version) + "/marketing_messages/status", options); + public Task> RetrieveEncryptionKeyAsync(string phone, string version, RequestOptions? options = null) => Get(Path(phone, version) + "/whatsapp_business_encryption", options); + public Task> RegisterEncryptionKeyAsync(string phone, RegisterFlowEncryptionKeyRequest body, string version, RequestOptions? options = null) => Post(Path(phone, version) + "/whatsapp_business_encryption", body, (options ?? new()) with { MaxNetworkRetries = 0 }); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CloudSessions.cs b/packages/dotnet/src/Polymorfa.Sdk/CloudSessions.cs new file mode 100644 index 00000000..ed51d0b1 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CloudSessions.cs @@ -0,0 +1,27 @@ +namespace Polymorfa.Sdk; + +public sealed record CustomerServiceWindow(string State, string? Reason, string? OpenedAt, string? ExpiresAt, string CheckedAt); +public sealed record MetaPricingParameters(string? Since = null, string? Until = null); +public sealed record MetaPricingGroup(string Category, string PricingModel, string? PricingType, bool? Billable, long Messages); +public sealed record MetaPricingSummary(string Source, string Since, string Until, long Messages, IReadOnlyList Groups); +public sealed record CloudCredentialTokenHealth(string Status, string? ExpiresAt); +public sealed record CloudCredentialHealth(string Status, string? CheckedAt, string? NextCheckAt, CloudCredentialTokenHealth Token, IReadOnlyList MissingPermissions, string PhoneRegistration, string WebhookSubscription, string? FailureCode); +public sealed record CloudReauthorization(string QuicklinkId, string Url, string Session) +{ + public override string ToString() => $"CloudReauthorization {{ QuicklinkId = {QuicklinkId}, Url = [REDACTED] }}"; +} +public sealed partial class MessagingSessions +{ + public Task>> GetMetaPricingAsync(string session, MetaPricingParameters? parameters = null, RequestOptions? options = null) + { + http.Credential.RequireServer(); return http.RequestAsync>(HttpMethod.Get, $"/messaging/{Uri.EscapeDataString(session)}/meta-pricing", null, options, Resource.Query(parameters)); + } + public Task>> GetCloudCredentialHealthAsync(string session, RequestOptions? options = null) + { + http.Credential.RequireServer(); return http.RequestAsync>(HttpMethod.Get, $"/messaging/{Uri.EscapeDataString(session)}/cloud-credentials", null, options); + } + public Task>> ReauthorizeCloudCredentialsAsync(string session, RequestOptions? options = null) + { + http.Credential.RequireServer(); return http.RequestAsync>(HttpMethod.Post, $"/messaging/{Uri.EscapeDataString(session)}/cloud-credentials/reauthorize", null, (options ?? new()) with { MaxNetworkRetries = 0 }); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/CommandResult.cs b/packages/dotnet/src/Polymorfa.Sdk/CommandResult.cs new file mode 100644 index 00000000..3a59cffc --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CommandResult.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +/// A completed RPC value or the request ID returned for asynchronous execution. +[JsonConverter(typeof(CommandResultConverter))] +public abstract record CommandResult; +public sealed record CompletedCommand(T Value) : CommandResult; +public sealed record AcceptedCommand(string RequestId) : CommandResult; +internal sealed class CommandResultConverter : JsonConverterFactory +{ + public override bool CanConvert(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(CommandResult<>); + public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) => (JsonConverter)Activator.CreateInstance(typeof(Converter<>).MakeGenericType(type.GetGenericArguments()))!; + private sealed class Converter : JsonConverter> + { + public override CommandResult Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + var json = document.RootElement; + if (json.ValueKind == JsonValueKind.Object && json.TryGetProperty("requestId", out var id) && id.ValueKind == JsonValueKind.String) return new AcceptedCommand(id.GetString()!); + return new CompletedCommand(json.Deserialize(options) ?? throw new JsonException("Invalid RPC response.")); + } + public override void Write(Utf8JsonWriter writer, CommandResult result, JsonSerializerOptions options) + { + if (result is CompletedCommand completed) JsonSerializer.Serialize(writer, completed.Value, options); + else if (result is AcceptedCommand accepted) JsonSerializer.Serialize(writer, new { accepted.RequestId }, options); + else throw new JsonException("Unknown RPC result."); + } + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Configuration.cs b/packages/dotnet/src/Polymorfa.Sdk/Configuration.cs new file mode 100644 index 00000000..f1f412d9 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Configuration.cs @@ -0,0 +1,89 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +public enum CredentialKind { OrganizationApiKey, ProjectToken, ClientToken } + +/// A principal with locally checked framing. Its string representation never contains the secret. +public sealed class Credential +{ + internal string Value { get; } + public CredentialKind Kind { get; } + private Credential(CredentialKind kind, string value) { Kind = kind; Value = value; } + public static Credential OrganizationApiKey(string value) => Create(CredentialKind.OrganizationApiKey, value, "pmfa_"); + public static Credential ProjectToken(string value) => Create(CredentialKind.ProjectToken, value, "pmfa_pt_"); + public static Credential ClientToken(string value) => Create(CredentialKind.ClientToken, value, "^pmfa_ct_[^\\r\\n]+$"); + private static Credential Create(CredentialKind kind, string value, string pattern) + { + if (value is null || !(kind == CredentialKind.ClientToken ? ClientShape(value) : OpaqueShape(value, pattern))) + throw new PolymorfaConfigurationException("Invalid credential."); + if (kind == CredentialKind.OrganizationApiKey && new[] { "pmfa_pt_", "pmfa_ct_", "pmfa_ls_", "pmfa_at_", "pmfa_wst_", "pmfa_sd_" }.Any(prefix => value.StartsWith(prefix, StringComparison.Ordinal))) + throw new PolymorfaConfigurationException("Special-purpose credentials cannot be used as team keys."); + if (kind != CredentialKind.ClientToken && OperatingSystem.IsBrowser()) + throw new PolymorfaConfigurationException("Server credentials cannot be used in browser/WASM applications."); + return new Credential(kind, value); + } + private static bool ClientShape(string value) + { + if (!value.StartsWith("pmfa_ct_", StringComparison.Ordinal)) return false; + var body = value[8..]; + if (!Regex.IsMatch(body, "^[A-Za-z0-9_-]+$", RegexOptions.CultureInvariant)) return false; + try + { + var raw = Convert.FromBase64String(body.Replace('-', '+').Replace('_', '/') + new string('=', (4 - body.Length % 4) % 4)); + return Convert.ToBase64String(raw).TrimEnd('=').Replace('+', '-').Replace('/', '_') == body; + } + catch (FormatException) { return false; } + } + // Shape validation only; the API authenticates signatures and records. + private static bool OpaqueShape(string value, string prefix) + { + if (!value.StartsWith(prefix, StringComparison.Ordinal)) return false; + var body = value[prefix.Length..]; + if (!Regex.IsMatch(body, "^[A-Za-z0-9_-]+$", RegexOptions.CultureInvariant)) return false; + try + { + var raw = Convert.FromBase64String(body.Replace('-', '+').Replace('_', '/') + new string('=', (4 - body.Length % 4) % 4)); + if (raw.Length < 24 || Convert.ToBase64String(raw).TrimEnd('=').Replace('+', '-').Replace('/', '_') != body) return false; + uint crc = 0xffffffff; + foreach (var b in raw.AsSpan(0, raw.Length - 4)) { crc ^= b; for (int i = 0; i < 8; i++) crc = (crc >> 1) ^ ((crc & 1) != 0 ? 0xedb88320u : 0u); } + return (crc ^ 0xffffffff) == System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(raw.AsSpan(raw.Length - 4)); + } + catch (FormatException) { return false; } + } + internal void RequireServer() + { + if (Kind == CredentialKind.ClientToken) throw new PolymorfaConfigurationException("This operation requires a server credential."); + if (OperatingSystem.IsBrowser()) throw new PolymorfaConfigurationException("Server credentials cannot be used in browser/WASM applications."); + } + public override string ToString() => $"Credential({Kind}, redacted)"; +} + +public sealed record ClientOptions +{ + public Uri BaseUrl { get; init; } = new("https://api.polymorfa.com"); + public string ApiVersion { get; init; } = SdkVersion.NativeApiVersion; + public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30); + public int MaxNetworkRetries { get; init; } = 2; + public IWebProxy? Proxy { get; init; } + /// An application-owned transport handler. Standard HTTP handlers must disable redirects; custom handlers must honor that policy. + public HttpMessageHandler? TransportHandler { get; init; } + /// Creates an independent credential-free storage handler. Standard handlers must disable redirects and cookies. The SDK disposes each returned handler. + public Func? StorageTransportHandlerFactory { get; init; } +} +public sealed record RequestOptions +{ + public string? ApiVersion { get; init; } + public IReadOnlyDictionary? Headers { get; init; } + public string? IdempotencyKey { get; init; } + public int? MaxNetworkRetries { get; init; } + public TimeSpan? Timeout { get; init; } + public CancellationToken CancellationToken { get; init; } + internal RequestOptions WithIdempotency() => IdempotencyHeaders.Prepare(this, generate: true); +} +public static class SdkVersion +{ + public const string Version = "0.1.0-dev.0"; + public const string NativeApiVersion = "2026-09-22"; +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/ConfigurationContract.cs b/packages/dotnet/src/Polymorfa.Sdk/ConfigurationContract.cs new file mode 100644 index 00000000..d62531dd --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/ConfigurationContract.cs @@ -0,0 +1,61 @@ +using System.Text.Json; + +namespace Polymorfa.Sdk; + +internal static class ConfigurationContract +{ + private static void Invalid(string field) => throw new PolymorfaConfigurationException("Invalid configuration field: " + field + "."); + internal static JsonElement Capture(object body, SessionConfigurationPatch patch, out bool intent) + { + var value = CampaignContractValidation.Capture(body); + var configuration = value.GetProperty("configuration"); + CampaignContractValidation.Fields(configuration, ["set", "reset"], []); + intent = false; + if (configuration.TryGetProperty("set", out var set)) + { + CampaignContractValidation.Fields(set, ["historySync", "observation", "hms"], []); + if (set.TryGetProperty("historySync", out var history)) { History(history, false); intent = history.TryGetProperty("delivery", out _); } + } + if (configuration.TryGetProperty("reset", out var reset)) + { + if (reset.ValueKind != JsonValueKind.Array) Invalid("reset"); + foreach (var item in reset.EnumerateArray()) + { + if (item.ValueKind != JsonValueKind.String || item.GetString() is not ("historySync" or "historySync.mode" or "historySync.requestFull" or "historySync.delivery" or "observation" or "observation.presenceMode" or "observation.typingMode" or "observation.labelMode" or "observation.quickReplyMode" or "hms" or "hms.enabled" or "hms.region" or "hms.policy" or "hms.retentionDays" or "hms.policyVersion" or "hms.legalHold")) Invalid("reset"); + if (item.GetString() == "historySync.delivery") intent = true; + } + } + return value; + } + private static void History(JsonElement history, bool quicklink) + { + CampaignContractValidation.Fields(history, quicklink ? ["mode", "requestFull", "delivery", "consent"] : ["mode", "requestFull", "delivery"], []); + if (history.TryGetProperty("delivery", out var delivery) && (delivery.ValueKind != JsonValueKind.String || delivery.GetString() != "descriptor")) Invalid("historySync.delivery"); + if (history.TryGetProperty("mode", out var mode) && (mode.ValueKind != JsonValueKind.String || mode.GetString() is not ("off" or "metadata_only" or "deliver"))) Invalid("historySync.mode"); + if (history.TryGetProperty("requestFull", out var full) && full.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) Invalid("historySync.requestFull"); + } + internal static JsonElement QuickLink(CreateQuickLinkRequest body, out bool intent) + { + var value = CampaignContractValidation.Capture(body); + CampaignContractValidation.Fields(value, ["purpose", "session", "projectId", "customerId", "externalId", "connectionGoal", "addConnection", "configuration", "billingControls", "signalMode"], []); + var purpose = body.Purpose ?? "initial"; + if (purpose is not ("initial" or "add_connection")) Invalid("purpose"); + if (purpose == "add_connection") + { + if (string.IsNullOrEmpty(body.Session) || body.AddConnection is not ("linked_devices" or "official_api") || body.CustomerId is not null || body.ExternalId is not null || body.BillingControls is not null || body.SignalMode.IsSpecified || body.ConnectionGoal is not (null or "hybrid")) Invalid("add_connection"); + } + else if (body.Session is not null || body.AddConnection is not null) Invalid("initial"); + intent = false; + if (value.TryGetProperty("signalMode", out var mode)) + { + if (mode.ValueKind != JsonValueKind.String || mode.GetString() is not ("standard" or "zero_knowledge")) Invalid("signalMode"); + intent = true; + } + if (value.TryGetProperty("configuration", out var config)) + { + CampaignContractValidation.Fields(config, ["connectionPreference", "connectionEnforcement", "methods", "defaultMethod", "prefillPhone", "allowPhoneChange", "historySync", "observation", "hms", "testing"], []); + if (config.TryGetProperty("historySync", out var history)) { History(history, true); intent |= history.TryGetProperty("delivery", out _); } + } + return value; + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/ConstrainedTypes.cs b/packages/dotnet/src/Polymorfa.Sdk/ConstrainedTypes.cs new file mode 100644 index 00000000..d2f1cd72 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/ConstrainedTypes.cs @@ -0,0 +1,41 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +internal abstract class KnownStringEnumConverter(IReadOnlyDictionary names) : JsonConverter where T : struct, Enum +{ + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + foreach (var pair in names) if (pair.Value == reader.GetString()) return pair.Key; + throw new JsonException($"Unknown {typeof(T).Name} value."); + } + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (!names.TryGetValue(value, out var name)) throw new JsonException($"Unknown {typeof(T).Name} value."); + writer.WriteStringValue(name); + } +} +[JsonConverter(typeof(OpenAiTtsModelConverter))] public enum OpenAiTtsModel { Gpt4oMiniTts, Tts1, Tts1Hd } +internal sealed class OpenAiTtsModelConverter() : KnownStringEnumConverter(new Dictionary { [OpenAiTtsModel.Gpt4oMiniTts] = "gpt-4o-mini-tts", [OpenAiTtsModel.Tts1] = "tts-1", [OpenAiTtsModel.Tts1Hd] = "tts-1-hd" }); +[JsonConverter(typeof(ElevenLabsTtsModelConverter))] public enum ElevenLabsTtsModel { MultilingualV2, FlashV25, TurboV25 } +internal sealed class ElevenLabsTtsModelConverter() : KnownStringEnumConverter(new Dictionary { [ElevenLabsTtsModel.MultilingualV2] = "eleven_multilingual_v2", [ElevenLabsTtsModel.FlashV25] = "eleven_flash_v2_5", [ElevenLabsTtsModel.TurboV25] = "eleven_turbo_v2_5" }); +[JsonConverter(typeof(OpenAiTtsVoiceConverter))] public enum OpenAiTtsVoice { Alloy, Ash, Ballad, Coral, Echo, Fable, Nova, Onyx, Sage, Shimmer, Verse } +internal sealed class OpenAiTtsVoiceConverter() : KnownStringEnumConverter(Enum.GetValues().ToDictionary(v => v, v => v.ToString().ToLowerInvariant())); +[JsonConverter(typeof(TemplateKindConverter))] public enum TemplateKind { Standard, Carousel, Authentication, LimitedTimeOffer } +internal sealed class TemplateKindConverter() : KnownStringEnumConverter(new Dictionary { [TemplateKind.Standard] = "standard", [TemplateKind.Carousel] = "carousel", [TemplateKind.Authentication] = "authentication", [TemplateKind.LimitedTimeOffer] = "limited_time_offer" }); +[JsonConverter(typeof(TemplateCategoryConverter))] public enum TemplateCategory { Marketing, Utility, Authentication } +internal sealed class TemplateCategoryConverter() : KnownStringEnumConverter(Enum.GetValues().ToDictionary(v => v, v => v.ToString().ToUpperInvariant())); +[JsonConverter(typeof(TemplateVariableTypeConverter))] public enum TemplateVariableType { Text, Number, Currency, DateTime } +internal sealed class TemplateVariableTypeConverter() : KnownStringEnumConverter(new Dictionary { [TemplateVariableType.Text] = "text", [TemplateVariableType.Number] = "number", [TemplateVariableType.Currency] = "currency", [TemplateVariableType.DateTime] = "date_time" }); +[JsonConverter(typeof(TemplateSurfaceConverter))] public enum TemplateSurface { Cloud, Whatsmeow, Sandbox } +internal sealed class TemplateSurfaceConverter() : KnownStringEnumConverter(Enum.GetValues().ToDictionary(v => v, v => v.ToString().ToLowerInvariant())); +[JsonConverter(typeof(TemplateOtpTypeConverter))] public enum TemplateOtpType { CopyCode, OneTap } +internal sealed class TemplateOtpTypeConverter() : KnownStringEnumConverter(new Dictionary { [TemplateOtpType.CopyCode] = "copy_code", [TemplateOtpType.OneTap] = "one_tap" }); +[JsonConverter(typeof(TemplateDefinitionVersionConverter))] public enum TemplateDefinitionVersion { V1 = 1 } +internal sealed class TemplateDefinitionVersionConverter : JsonConverter +{ + public override TemplateDefinitionVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out var version) && version == 1 ? TemplateDefinitionVersion.V1 : throw new JsonException("Template definition version must be 1."); + public override void Write(Utf8JsonWriter writer, TemplateDefinitionVersion value, JsonSerializerOptions options) { if (value != TemplateDefinitionVersion.V1) throw new JsonException("Template definition version must be 1."); writer.WriteNumberValue(1); } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/ContentDisposition.cs b/packages/dotnet/src/Polymorfa.Sdk/ContentDisposition.cs new file mode 100644 index 00000000..850b2e27 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/ContentDisposition.cs @@ -0,0 +1,57 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +/// Parses a display filename; never treat it as an authorized path. +public static partial class ContentDisposition +{ + public static string? ParseFilename(string? header) + { + if (header is null || header.Length > 8192) return null; + var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + var index = header.IndexOf(';'); + while (index >= 0 && index < header.Length) + { + index++; while (index < header.Length && header[index] is ' ' or '\t') index++; + var equals = header.IndexOf('=', index); if (equals < 0) break; + var name = header[index..equals].Trim(); index = equals + 1; + while (index < header.Length && header[index] is ' ' or '\t') index++; + string value; + if (index < header.Length && header[index] == '"') + { + index++; var text = new StringBuilder(); + while (index < header.Length && header[index] != '"') { if (header[index] == '\\' && index + 1 < header.Length) index++; text.Append(header[index++]); } + value = text.ToString(); index = header.IndexOf(';', index); + } + else { var end = header.IndexOf(';', index); value = header[index..(end < 0 ? header.Length : end)].Trim(); index = end; } + if (name.Length > 0) parameters.TryAdd(name, value); + } + if (parameters.TryGetValue("filename*", out var extended) && DecodeExtended(extended) is { } decoded) return Sanitize(decoded); + return parameters.TryGetValue("filename", out var plain) ? Sanitize(plain) : null; + } + private static string? DecodeExtended(string value) + { + var match = Extended().Match(value); if (!match.Success) return null; + var charset = match.Groups[1].Value.ToLowerInvariant(); var encoded = match.Groups[2].Value; + if (!Encoded().IsMatch(encoded)) return null; + var bytes = new List(); + for (var i = 0; i < encoded.Length; i++) + { + if (encoded[i] == '%') { bytes.Add(Convert.ToByte(encoded.Substring(i + 1, 2), 16)); i += 2; } + else bytes.Add((byte)encoded[i]); + } + try { return charset switch { "utf-8" => new UTF8Encoding(false, true).GetString(bytes.ToArray()), "iso-8859-1" => Encoding.Latin1.GetString(bytes.ToArray()), _ => null }; } + catch (DecoderFallbackException) { return null; } + } + private static string? Sanitize(string value) + { + var last = Math.Max(value.LastIndexOf('/'), value.LastIndexOf('\\')); + var cleaned = new string(value[(last + 1)..].Where(character => character >= 0x20 && character != 0x7f).ToArray()).Trim(); + return cleaned is "" or "." or ".." ? null : cleaned; + } + [GeneratedRegex("^([A-Za-z0-9!#$%&+\\-^_`{}~]+)'[A-Za-z0-9-]*'(.*)$")] + private static partial Regex Extended(); + [GeneratedRegex("^(?:[A-Za-z0-9!#$&+.^_`|~-]|%[0-9A-Fa-f]{2})*$")] + private static partial Regex Encoded(); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Customers.cs b/packages/dotnet/src/Polymorfa.Sdk/Customers.cs new file mode 100644 index 00000000..abcfbfb4 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Customers.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +public record Customer(string Id, string OrgId, string ProjectId, string? Name, string? ExternalCustomerId, string Status, bool IsDefault, long? ArchivedAt, long CreatedAt, long UpdatedAt); +public sealed record CustomerSummary(string Id, string OrgId, string ProjectId, string? Name, string? ExternalCustomerId, string Status, bool IsDefault, long? ArchivedAt, long CreatedAt, long UpdatedAt, int NumberCount, int ConnectedNumberCount, string? ActivePairingLinkState, long? LastActivityAt, bool NeedsAttention) : Customer(Id, OrgId, ProjectId, Name, ExternalCustomerId, Status, IsDefault, ArchivedAt, CreatedAt, UpdatedAt); +public sealed record CustomersStatus(bool Enabled, long? EnabledAt, string? EnabledBy, Customer? DefaultCustomer); +public sealed record CustomersEnablement(bool Enabled, long? EnabledAt, string? EnabledBy, Customer? DefaultCustomer, int MigratedNumberCount); +public sealed record CustomerProjectRequest(string? ProjectId = null); +public sealed record CreateCustomerRequest(string? ProjectId = null, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue Name = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue ExternalCustomerId = default); +public sealed record UpdateCustomerRequest(string? ProjectId = null, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue Name = default, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue ExternalCustomerId = default); +public sealed record ListCustomersParameters(string ProjectId, string? Cursor = null, int? Limit = null, string? Search = null, string? Status = null, bool? IsDefault = null, bool? HasNumbers = null, bool? NeedsAttention = null); +public sealed record CustomerListPage(string? NextCursor, bool HasMore); +public sealed record CustomerListEnvelope(IReadOnlyList Data, CustomerListPage Page); +public sealed record CustomerNumber(string Id, string CustomerId, string SessionId, string? Name, string? PhoneMasked, string Status, string? Backend, long CreatedAt); +public sealed record CustomerEventMetadata(IReadOnlyList? Fields = null); +public sealed record CustomerEvent(string Id, string Action, string? FromStatus, string? ToStatus, string? SessionId, string? PairingLinkId, CustomerEventMetadata Metadata, long OccurredAt); +public record CustomerPairingLink(string Id, string OrgId, string ProjectId, string CustomerId, string? ExpectedPhoneMasked, IReadOnlyList Methods, string? Locale, string? Theme, long ExpiresAt, string Status, int AttemptCount, int MaxAttempts, string? PendingSessionId, string? CreatedBy, long? ReservedAt, long? OpenedAt, long? ConnectingAt, long? ConnectedAt, long? FailedAt, long? ExpiredAt, long? RevokedAt, string? LastErrorCode, int FailedExchangeCount, int PhoneMismatchCount, long CreatedAt, long UpdatedAt); +public sealed record CreatedCustomerPairingLink(string Id, string OrgId, string ProjectId, string CustomerId, string? ExpectedPhoneMasked, IReadOnlyList Methods, string? Locale, string? Theme, long ExpiresAt, string Status, int AttemptCount, int MaxAttempts, string? PendingSessionId, string? CreatedBy, long? ReservedAt, long? OpenedAt, long? ConnectingAt, long? ConnectedAt, long? FailedAt, long? ExpiredAt, long? RevokedAt, string? LastErrorCode, int FailedExchangeCount, int PhoneMismatchCount, long CreatedAt, long UpdatedAt, string? Url) : CustomerPairingLink(Id, OrgId, ProjectId, CustomerId, ExpectedPhoneMasked, Methods, Locale, Theme, ExpiresAt, Status, AttemptCount, MaxAttempts, PendingSessionId, CreatedBy, ReservedAt, OpenedAt, ConnectingAt, ConnectedAt, FailedAt, ExpiredAt, RevokedAt, LastErrorCode, FailedExchangeCount, PhoneMismatchCount, CreatedAt, UpdatedAt) +{ + public override string ToString() => $"CreatedCustomerPairingLink {{ Id = {Id}, Status = {Status}, Url = [REDACTED] }}"; +} +public sealed record CreateCustomerPairingLinkRequest(string? ProjectId = null, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue ExpectedPhone = default, IReadOnlyList? Methods = null, int? ExpiresInSeconds = null); +public sealed record ListCustomerEventsParameters(string ProjectId, int? Limit = null); +public sealed record TransferCustomerNumberRequest(string ProjectId, string SourceCustomerId, bool Confirm); + +public sealed partial class Customers : Resource +{ + internal Customers(HttpTransport http) : base(http) { } + private static string Path(string customer) => "/platform/customers/" + E(customer); + public Task>> StatusAsync(string projectId, RequestOptions? options = null) => Get>($"/platform/projects/{E(projectId)}/customers/status", options); + public Task>> EnableAsync(string projectId, RequestOptions? options = null) => Post>($"/platform/projects/{E(projectId)}/customers/enable", null, options); + public Task> ListAsync(ListCustomersParameters parameters, RequestOptions? options = null) => Get("/platform/customers", options, parameters); + public Task>> CreateAsync(CreateCustomerRequest? body = null, RequestOptions? options = null) => Post>("/platform/customers", body ?? new(), options); + public Task>> RetrieveAsync(string customerId, string projectId, RequestOptions? options = null) => Get>(Path(customerId), options, new { projectId }); + public Task>> UpdateAsync(string customerId, UpdateCustomerRequest body, RequestOptions? options = null) => Patch>(Path(customerId), body, options); + public Task>> ArchiveAsync(string customerId, CustomerProjectRequest? body = null, RequestOptions? options = null) => Post>(Path(customerId) + "/archive", body ?? new(), options); + public Task>> RestoreAsync(string customerId, CustomerProjectRequest? body = null, RequestOptions? options = null) => Post>(Path(customerId) + "/restore", body ?? new(), options); + public Task>>> ListNumbersAsync(string customerId, string projectId, RequestOptions? options = null) => Get>>(Path(customerId) + "/numbers", options, new { projectId }); + public Task>>> ListEventsAsync(string customerId, ListCustomerEventsParameters parameters, RequestOptions? options = null) => Get>>(Path(customerId) + "/events", options, parameters); + public Task>> CreatePairingLinkAsync(string customerId, CreateCustomerPairingLinkRequest? body = null, RequestOptions? options = null) => Post>(Path(customerId) + "/pairing-links", body ?? new(), options); + public Task>>> ListPairingLinksAsync(string customerId, string projectId, RequestOptions? options = null) => Get>>(Path(customerId) + "/pairing-links", options, new { projectId }); + public Task>> RevokePairingLinkAsync(string customerId, string linkId, string projectId, RequestOptions? options = null) => Http.RequestAsync>(HttpMethod.Delete, Path(customerId) + "/pairing-links/" + E(linkId), null, options, Query(new { projectId })); + public Task>> TransferNumberAsync(string customerId, string sessionId, TransferCustomerNumberRequest body, RequestOptions? options = null) + { + if (!body.Confirm) throw new PolymorfaValidationException("Number transfer requires confirm:true.", "invalid_parameter"); + return Post>(Path(customerId) + "/numbers/" + E(sessionId) + "/transfer", body, options); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/DirectHistoryEvents.cs b/packages/dotnet/src/Polymorfa.Sdk/DirectHistoryEvents.cs new file mode 100644 index 00000000..8c8914a9 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/DirectHistoryEvents.cs @@ -0,0 +1,108 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +/// A projected notification. Parsing does not download or store history. +public abstract record DirectHistorySyncPayload( + [property: JsonPropertyName("whatsapp_ids")] WhatsAppMessageIds WhatsAppIds, + string SyncType, double FileLength, double? ChunkOrder = null, int? Progress = null, + string? OldestMessageAt = null, string? PeerDataRequestSessionId = null, + [property: JsonPropertyName("original_whatsapp_ids")] WhatsAppMessageIds? OriginalWhatsAppIds = null, + [property: JsonPropertyName("whatsapp_id")] string? WhatsAppId = null, + [property: JsonPropertyName("original_whatsapp_id")] string? OriginalWhatsAppId = null) +{ + public string Delivery => "descriptor"; +} +public sealed record DirectHistoryMedia(string DirectPath, string MediaKey, string FileSha256, string FileEncSha256, string? EncHandle = null) +{ + public string Type => "md-msg-hist"; + public override string ToString() => "DirectHistoryMedia(redacted)"; +} +public sealed record DirectHistoryInline(string Data) +{ + public string Encoding => "zlib-base64-protobuf"; + public override string ToString() => "DirectHistoryInline(redacted)"; +} +public sealed record DirectHistorySyncMediaPayload(WhatsAppMessageIds WhatsAppIds, string SyncType, double FileLength, DirectHistoryMedia Media, double? ChunkOrder = null, int? Progress = null, string? OldestMessageAt = null, string? PeerDataRequestSessionId = null, WhatsAppMessageIds? OriginalWhatsAppIds = null, string? WhatsAppId = null, string? OriginalWhatsAppId = null) : DirectHistorySyncPayload(WhatsAppIds, SyncType, FileLength, ChunkOrder, Progress, OldestMessageAt, PeerDataRequestSessionId, OriginalWhatsAppIds, WhatsAppId, OriginalWhatsAppId) +{ + public override string ToString() => "DirectHistorySyncMediaPayload(redacted)"; +} +public sealed record DirectHistorySyncInlinePayload(WhatsAppMessageIds WhatsAppIds, string SyncType, double FileLength, DirectHistoryInline Inline, double? ChunkOrder = null, int? Progress = null, string? OldestMessageAt = null, string? PeerDataRequestSessionId = null, WhatsAppMessageIds? OriginalWhatsAppIds = null, string? WhatsAppId = null, string? OriginalWhatsAppId = null) : DirectHistorySyncPayload(WhatsAppIds, SyncType, FileLength, ChunkOrder, Progress, OldestMessageAt, PeerDataRequestSessionId, OriginalWhatsAppIds, WhatsAppId, OriginalWhatsAppId) +{ + public override string ToString() => "DirectHistorySyncInlinePayload(redacted)"; +} +internal static class DirectHistoryEvents +{ + internal static WebhookEvent Parse(WebhookEnvelope envelope) + { + try + { + var p = envelope.Payload; + Fields(p, ["delivery", "whatsapp_ids", "whatsapp_id", "original_whatsapp_ids", "original_whatsapp_id", "syncType", "chunkOrder", "progress", "fileLength", "oldestMessageAt", "peerDataRequestSessionId", "media", "inline"]); + if (String(p.GetProperty("delivery")) != "descriptor" || p.TryGetProperty("media", out _) == p.TryGetProperty("inline", out _)) throw new JsonException(); + var ids = Ids(p.GetProperty("whatsapp_ids")); + var original = p.TryGetProperty("original_whatsapp_ids", out var o) ? Ids(o) : null; + var sync = String(p.GetProperty("syncType")); + var length = Number(p.GetProperty("fileLength")); + var order = p.TryGetProperty("chunkOrder", out var c) ? Number(c) : (double?)null; + var progress = p.TryGetProperty("progress", out var progressJson) ? Number(progressJson) : (double?)null; + if (progress > 100) throw new JsonException(); + var oldest = OptionalString(p, "oldestMessageAt"); + if (oldest is not null && !IsoDateTime(oldest)) throw new JsonException(); + var peer = OptionalString(p, "peerDataRequestSessionId", 0); + var alias = OptionalString(p, "whatsapp_id", 1, 512); + var originalAlias = OptionalString(p, "original_whatsapp_id", 1, 512); + if (p.TryGetProperty("media", out var media)) + { + Fields(media, ["type", "directPath", "mediaKey", "fileSha256", "fileEncSha256", "encHandle"]); + if (String(media.GetProperty("type")) != "md-msg-hist") throw new JsonException(); + var payload = new DirectHistorySyncMediaPayload(ids, sync, length, new(String(media.GetProperty("directPath")), String(media.GetProperty("mediaKey")), String(media.GetProperty("fileSha256")), String(media.GetProperty("fileEncSha256")), OptionalString(media, "encHandle", 0)), order, (int?)progress, oldest, peer, original, alias, originalAlias); + return new TypedWebhookEvent(envelope, payload); + } + var inline = p.GetProperty("inline"); Fields(inline, ["encoding", "data"]); + if (String(inline.GetProperty("encoding")) != "zlib-base64-protobuf") throw new JsonException(); + return new TypedWebhookEvent(envelope, new(ids, sync, length, new(String(inline.GetProperty("data"))), order, (int?)progress, oldest, peer, original, alias, originalAlias)); + } + catch (Exception error) when (error is JsonException or InvalidOperationException or KeyNotFoundException or FormatException or OverflowException) + { + throw new PolymorfaValidationException("Invalid descriptor history notification.", "invalid_webhook_event"); + } + } + private static bool IsoDateTime(string value) + { + // f97 uses Zod 3.25.76's default datetime predicate: UTC, optional + // seconds/fraction and a four-digit Gregorian year, including year zero. + var match = Regex.Match(value, "\\A([0-9]{4})-(0[1-9]|1[0-2])-([0-9]{2})T(?:[01][0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9](?:\\.[0-9]+)?)?Z\\z", RegexOptions.CultureInvariant); + if (!match.Success) return false; + var year = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + var month = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); + var day = int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture); + int[] days = [31, year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + return day >= 1 && day <= days[month - 1]; + } + private static void Fields(JsonElement value, string[] allowed) + { + if (value.ValueKind != JsonValueKind.Object || value.EnumerateObject().Any(p => !allowed.Contains(p.Name)) || value.EnumerateObject().GroupBy(p => p.Name).Any(g => g.Count() > 1)) throw new JsonException(); + } + private static string String(JsonElement value, int min = 1, int max = int.MaxValue) + { + if (value.ValueKind != JsonValueKind.String || value.GetString() is not { } text || text.Length < min || text.Length > max) throw new JsonException(); + return text; + } + private static string? OptionalString(JsonElement value, string name, int min = 1, int max = int.MaxValue) => value.TryGetProperty(name, out var p) ? String(p, min, max) : null; + private static double Number(JsonElement value) + { + if (value.ValueKind != JsonValueKind.Number || !value.TryGetDouble(out var number) || !double.IsFinite(number) || number < 0 || Math.Truncate(number) != number) throw new JsonException(); + return number; + } + private static WhatsAppMessageIds Ids(JsonElement value) + { + Fields(value, ["linked_devices", "official_api"]); + var linked = OptionalString(value, "linked_devices", 1, 512); var cloud = OptionalString(value, "official_api", 1, 512); + if (linked is null && cloud is null) throw new JsonException(); + return new(linked, cloud); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/DirectMedia.cs b/packages/dotnet/src/Polymorfa.Sdk/DirectMedia.cs new file mode 100644 index 00000000..7790543a --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/DirectMedia.cs @@ -0,0 +1,116 @@ +using System.Net.Http.Headers; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +public sealed record WhatsAppUploadGrant(IReadOnlyList Hosts, Uri UploadUrl, string Auth, DateTimeOffset ExpiresAt) +{ + public override string ToString() => "WhatsAppUploadGrant(redacted)"; +} +public sealed record WhatsAppUploadLocation(string DirectPath, string Url, string? Handle = null) +{ + public override string ToString() => "WhatsAppUploadLocation(redacted)"; +} +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum HistorySyncType { INITIAL_BOOTSTRAP, INITIAL_STATUS_V3, FULL, RECENT, PUSH_NAME, NON_BLOCKING_DATA, ON_DEMAND } +public sealed record HistoryChunkRetryRequest(HistorySyncType SyncType, uint ChunkOrder, string? ChunkMessageId = null, bool? Regenerate = null); +public sealed record HistoryChunkRetryResult(string RequestId, string Status); +public sealed record HistoryChunkDeleteRequest(string DirectPath, string FileEncSha256, string? EncHandle = null, string? CompanionMetaNonce = null) +{ + public override string ToString() => "HistoryChunkDeleteRequest(redacted)"; +} +public sealed partial class WhatsAppMediaResource +{ + public const string CustomerOwnedApiVersion = "2026-10-14"; + internal static RequestOptions Version(RequestOptions? options) => (options ?? new()) with { ApiVersion = options?.ApiVersion ?? CustomerOwnedApiVersion }; + // These four held API routes have no replay ledger, even with a caller key. + internal static RequestOptions SubmissionOptions(RequestOptions? options) => Version(options) with { MaxNetworkRetries = 0 }; + private readonly HttpTransport http; + internal WhatsAppMediaResource(HttpTransport http) => this.http = http; + private static string EncodedHash(byte[] hash) { if (hash is null || hash.Length != 32) throw new PolymorfaValidationException("fileEncSha256 must contain 32 bytes.", "invalid_parameter"); return Convert.ToBase64String(hash).TrimEnd('=').Replace('+', '-').Replace('/', '_'); } + internal static string ApiKind(WhatsAppUploadMediaType kind) => kind switch { WhatsAppUploadMediaType.Audio or WhatsAppUploadMediaType.Ptt => "voice", WhatsAppUploadMediaType.Document => "file", _ => WhatsAppMedia.UploadKind(kind) }; + internal static string UploadPath(WhatsAppUploadMediaType kind) => "/mms/" + WhatsAppMedia.UploadKind(kind); + internal static JsonElement Data(JsonElement value) => value.ValueKind == JsonValueKind.Object && value.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Object ? data : value; + private static object GrantBody(WhatsAppUploadMediaType kind, string hash) => kind is WhatsAppUploadMediaType.Audio or WhatsAppUploadMediaType.Ptt ? (object)new { type = "voice", fileEncSha256 = hash, ptt = kind == WhatsAppUploadMediaType.Ptt } : new { type = ApiKind(kind), fileEncSha256 = hash }; + public async Task CreateUploadGrantAsync(string session, WhatsAppUploadMediaType kind, byte[] fileEncSha256, RequestOptions? options = null) + { + http.Credential.RequireServer(); + Session(session); + var hash = EncodedHash(fileEncSha256); + var response = await http.RequestAsync(HttpMethod.Post, $"/messaging/{Uri.EscapeDataString(session)}/media/upload-grants", GrantBody(kind, hash), SubmissionOptions(options)).ConfigureAwait(false); + try + { + var body = CanonicalData(response); var url = new Uri(body.GetProperty("uploadUrl").GetString()!); var host = body.GetProperty("host").GetString(); + var auth = url.Query.TrimStart('?').Split('&').Select(pair => pair.Split('=', 2)).Where(pair => pair.Length == 2 && Uri.UnescapeDataString(pair[0]) == "auth").Select(pair => Uri.UnescapeDataString(pair[1])).FirstOrDefault(); + if (!WhatsAppMedia.IsMediaUrl(url.AbsoluteUri) || url.Host != host || string.IsNullOrEmpty(auth) || url.AbsolutePath != UploadPath(kind) + "/" + hash) throw InvalidResponse(response.Metadata); + var expirySeconds = body.GetProperty("expiresAt").GetInt64(); if (expirySeconds <= 0) throw InvalidResponse(response.Metadata); var expiry = DateTimeOffset.FromUnixTimeSeconds(expirySeconds); + return new([host!], url, auth, expiry); + } + catch (Exception error) when (error is JsonException or InvalidOperationException or KeyNotFoundException or ArgumentException or FormatException) { throw InvalidResponse(response.Metadata); } + } + /// Transfers only prepared ciphertext. Keeps the caller's input open and never retries the API upload. + public async Task RelayUploadAsync(string session, WhatsAppUploadMediaType kind, byte[] fileEncSha256, Stream encrypted, long encryptedLength, RequestOptions? options = null) + { + Session(session); + if (encryptedLength < 1 || !encrypted.CanRead) throw new PolymorfaValidationException("Invalid encrypted body or length.", "invalid_parameter"); + var query = new List> { new("type", ApiKind(kind)), new("fileEncSha256", EncodedHash(fileEncSha256)) }; + if (kind is WhatsAppUploadMediaType.Audio or WhatsAppUploadMediaType.Ptt) query.Add(new("ptt", (kind == WhatsAppUploadMediaType.Ptt).ToString().ToLowerInvariant())); + using var body = new StreamContent(new BorrowedCiphertext(encrypted)); body.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); body.Headers.ContentLength = encryptedLength; + var response = await http.RequestBinaryAsync($"/messaging/{Uri.EscapeDataString(session)}/media/uploads", body, SubmissionOptions(options), query).ConfigureAwait(false); + try { var data = CanonicalData(response); if (data.TryGetProperty("handle", out var handle) && handle.ValueKind != JsonValueKind.String) throw new JsonException(); var result = data.Deserialize(HttpTransport.Json) ?? throw InvalidResponse(response.Metadata); if (string.IsNullOrEmpty(result.DirectPath) || string.IsNullOrEmpty(result.Url)) throw InvalidResponse(response.Metadata); return result; } + catch (JsonException) { throw InvalidResponse(response.Metadata); } + } + internal static PolymorfaException InvalidResponse(ResponseMetadata? metadata = null) => new PolymorfaServerException("The API returned an invalid media/history response.", "invalid_response", metadata); + internal static void Session(string session) { if (string.IsNullOrEmpty(session)) throw new PolymorfaConfigurationException("A session is required."); CampaignContractValidation.Unicode(session); } + internal static JsonElement CanonicalData(ApiResponse response) + { + ExistingContractCodec.CheckMetadata(response.Metadata); + var value = response.Data; + if (value.ValueKind != JsonValueKind.Object || !value.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True || !value.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Object) throw new JsonException(); + return data; + } + private sealed class BorrowedCiphertext(Stream source) : Stream + { + public override bool CanRead => source.CanRead; public override bool CanWrite => false; public override bool CanSeek => false; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public override int Read(byte[] buffer, int offset, int count) => source.Read(buffer, offset, count); + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => source.ReadAsync(buffer, cancellationToken); + public override void Flush() { } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } +} +public sealed class HistorySyncResource +{ + private readonly HttpTransport http; + internal HistorySyncResource(HttpTransport http) => this.http = http; + public async Task RequestChunkRetryAsync(string session, HistoryChunkRetryRequest body, RequestOptions? options = null) + { + http.Credential.RequireServer(); WhatsAppMediaResource.Session(session); + if (body is null || !Enum.IsDefined(body.SyncType) || body.ChunkMessageId is { } id && !Regex.IsMatch(id, "\\A[A-Za-z0-9._:-]{1,128}\\z", RegexOptions.CultureInvariant)) throw new PolymorfaConfigurationException("Invalid history retry request."); + var captured = ExistingContractCodec.Capture(body); + var response = await http.RequestAsync(HttpMethod.Post, $"/messaging/{Uri.EscapeDataString(session)}/history/chunks/retry", captured, WhatsAppMediaResource.SubmissionOptions(options)).ConfigureAwait(false); + try + { + var data = WhatsAppMediaResource.CanonicalData(response); + var requestId = data.GetProperty("requestId"); var status = data.GetProperty("status"); + if (requestId.ValueKind != JsonValueKind.String || string.IsNullOrEmpty(requestId.GetString()) || status.ValueKind != JsonValueKind.String || status.GetString() != "requested") throw new JsonException(); + return new(requestId.GetString()!, "requested"); + } + catch (Exception error) when (error is JsonException or InvalidOperationException or KeyNotFoundException) { throw WhatsAppMediaResource.InvalidResponse(response.Metadata); } + } + public async Task DeleteChunkAsync(string session, HistoryChunkDeleteRequest body, RequestOptions? options = null) + { + http.Credential.RequireServer(); WhatsAppMediaResource.Session(session); + if (body is null || body.DirectPath is null || body.DirectPath.Length > 1024 || !Regex.IsMatch(body.DirectPath, "\\A/[A-Za-z0-9._~%/=&?+\\-]+\\z", RegexOptions.CultureInvariant) || body.FileEncSha256 is null || !Regex.IsMatch(body.FileEncSha256, "\\A[A-Za-z0-9+/]{43}=\\z", RegexOptions.CultureInvariant)) throw new PolymorfaConfigurationException("Invalid history deletion request."); + if (Convert.FromBase64String(body.FileEncSha256).Length != 32) throw new PolymorfaConfigurationException("Invalid history encrypted hash."); + foreach (var token in new[] { body.EncHandle, body.CompanionMetaNonce }) if (token is not null && !Regex.IsMatch(token, "\\A[A-Za-z0-9._~%/=+\\-]{1,512}\\z", RegexOptions.CultureInvariant)) throw new PolymorfaConfigurationException("Invalid history deletion token."); + var response = await http.RequestAsync(HttpMethod.Post, $"/messaging/{Uri.EscapeDataString(session)}/history/media/delete", ExistingContractCodec.Capture(body), WhatsAppMediaResource.SubmissionOptions(options)).ConfigureAwait(false); + try + { + var data = WhatsAppMediaResource.CanonicalData(response); + if (data.GetProperty("status").ValueKind != JsonValueKind.String || data.GetProperty("status").GetString() != "deleted") throw new JsonException(); + } + catch (Exception error) when (error is JsonException or InvalidOperationException or KeyNotFoundException) { throw WhatsAppMediaResource.InvalidResponse(response.Metadata); } + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/ErrorCodes.cs b/packages/dotnet/src/Polymorfa.Sdk/ErrorCodes.cs new file mode 100644 index 00000000..a9b6d5d8 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/ErrorCodes.cs @@ -0,0 +1,13 @@ +namespace Polymorfa.Sdk; + +/// Known API codes in the pinned contract. Exceptions preserve future codes as strings. +public static class PolymorfaErrorCodes +{ + public static IReadOnlyList Known { get; } = Array.AsReadOnly(new[] { + "invalid_parameter", "invalid_credential", "entitlement_required", "cloud_template_authority_restricted", "gate_limit_reached", "method_not_allowed", "permission_denied", "missing_scope", "resource_not_found", "hms_not_enabled", "media_fetch_failed", "resource_gone", "state_conflict", "rate_limit_exceeded", "internal_error", "operation_not_supported", "upstream_failure", "service_unavailable", "credential_verification_unavailable", "session_not_ready", + "hybrid_routing_paused", "hybrid_automation_unavailable", "hybrid_transport_unavailable", "hybrid_target_reference_unavailable", "hybrid_choice_required", "hybrid_transition_ineligible", "send_outcome_unknown", "command_dispatch_failed", "result_unknown", "recipient_not_on_whatsapp", "conversation_window_closed", "template_not_approved", "media_too_large", "whatsapp_rate_limited", "new_chat_limit_reached", "whatsapp_account_restricted", "whatsapp_groups_ineligible", "whatsapp_group_creation_paused", "whatsapp_group_limit_reached", "whatsapp_group_full", "whatsapp_group_suspended", "whatsapp_group_has_no_participants", "order_status_transition_invalid", "order_cancellation_failed", "number_restricted", + "bansafe_suspended", "bansafe_org_suspended", "bansafe_cold_blocked", "bansafe_cold_held", "bansafe_throttled", "bansafe_daily_allowance_reached", "bansafe_accounting_unavailable", "bansafe_send_outcome_unknown", "campaign_throughput_capped", "addon_required", "campaigns_not_entitled", "idempotency_completed", "idempotency_conflict", "idempotency_in_progress", "idempotency_outcome_unknown", "feature_unavailable", "stream_connection_limit_reached", "stream_cursor_expired", "stream_cursor_invalid", "payg_required", "premium_required", + "call_claimed", "call_not_ringing", "call_permission_required", "calls_disabled", "call_recipient_opted_out", "call_destination_blocked", "call_permission_request_limited", "call_permission_granted", "call_opt_out_limit", "asset_not_ready", "connection_limit", "invalid_sip_trunk", "sip_trunk_in_use", "sip_trunk_limit", "sip_trunk_revision_conflict", "sip_unavailable", "provider_credential_invalid", "provider_unavailable", "voice_asset_in_use", "voice_asset_revision_conflict", "voice_not_enabled", "voice_unavailable", "unsupported_for_connection", "whatsapp_authorization_required","whatsapp_payment_method_required","whatsapp_number_not_registered","marketing_disabled_on_cloud_api","parent_bsuid_not_supported","hybrid_group_linked_devices_unavailable","catalog_cache_full","catalog_sync_cooldown", "campaign_approval_required", "campaign_approval_stale", "campaign_variables_missing", "zero_knowledge_session" + }); + public static bool IsKnown(string? code) => code is not null && Known.Contains(code, StringComparer.Ordinal); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Errors.cs b/packages/dotnet/src/Polymorfa.Sdk/Errors.cs new file mode 100644 index 00000000..98e366ae --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Errors.cs @@ -0,0 +1,79 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +public class PolymorfaException : Exception +{ + public string? Code { get; } + public string? Param { get; } + public int? Status { get; } + public string? RequestId { get; } + public string? RequestLogUrl { get; } + public string? DocUrl { get; internal set; } + public string? RateLimitReason { get; } + public JsonElement? Details { get; } + public ResponseMetadata? Metadata { get; internal set; } + public PolymorfaException(string message, string? code = null, ResponseMetadata? metadata = null, JsonElement? error = null) : base(RedactText(message)) + { + Code = code; Param = String(error, "param"); Metadata = metadata; Status = metadata?.Status; + RequestId = String(error, "request_id") ?? metadata?.RequestId; + RequestLogUrl = String(error, "request_log_url"); DocUrl = String(error, "docs"); + RateLimitReason = metadata?.Headers.GetValueOrDefault("polymorfa-ratelimit-reason"); + Details = error is { ValueKind: JsonValueKind.Object } value && value.TryGetProperty("details", out var detail) ? RedactDetails(detail) : null; + } + private static string RedactText(string value) => Regex.Replace(value, "pmfa_[A-Za-z0-9_-]+", "[REDACTED]"); + private static JsonElement RedactDetails(JsonElement value) + { + JsonNode? Clean(JsonNode? node) + { + if (node is JsonObject obj) + { + foreach (var key in obj.Select(pair => pair.Key).ToArray()) + { + var normalized = Regex.Replace(key.ToLowerInvariant(), "[^a-z0-9]", ""); + obj[key] = new[] { "authorization", "apikey", "projecttoken", "clienttoken", "credential", "token", "secret" }.Contains(normalized) ? JsonValue.Create("[REDACTED]") : Clean(obj[key]?.DeepClone()); + } + } + else if (node is JsonArray array) for (var i = 0; i < array.Count; i++) array[i] = Clean(array[i]?.DeepClone()); + else if (node is JsonValue scalar && scalar.TryGetValue(out var text)) return JsonValue.Create(RedactText(text)); + return node; + } + return JsonSerializer.SerializeToElement(Clean(JsonNode.Parse(value.GetRawText()))); + } + private static string? String(JsonElement? value, string name) => value is { ValueKind: JsonValueKind.Object } element && element.TryGetProperty(name, out var property) && property.ValueKind == JsonValueKind.String ? property.GetString() : null; + internal static PolymorfaException FromResponse(JsonElement body, ResponseMetadata metadata) + { + var error = body.ValueKind == JsonValueKind.Object && body.TryGetProperty("error", out var nested) && nested.ValueKind == JsonValueKind.Object ? nested : body; + var message = String(error, "message") ?? $"Polymorfa returned HTTP {metadata.Status}."; + var code = String(error, "code"); + var exception = metadata.Status switch + { + 400 or 413 or 422 => new PolymorfaValidationException(message, code, metadata, error), + 401 => new PolymorfaAuthenticationException(message, code, metadata, error), + 402 => new PolymorfaPaymentRequiredException(message, code, metadata, error), + 403 => new PolymorfaAuthorizationException(message, code, metadata, error), + 404 => new PolymorfaNotFoundException(message, code, metadata, error), + 409 => new PolymorfaConflictException(message, code, metadata, error), + 429 => new PolymorfaRateLimitException(message, code, metadata, error), + >= 500 => new PolymorfaServerException(message, code, metadata, error), + _ => new PolymorfaException(message, code, metadata, error) + }; + exception.DocUrl = String(body, "docs") ?? exception.DocUrl; + return exception; + } +} +public sealed class PolymorfaConfigurationException(string message) : PolymorfaException(message, "invalid_configuration"); +public sealed class PolymorfaAuthenticationException(string message, string? code = null, ResponseMetadata? metadata = null, JsonElement? details = null) : PolymorfaException(message, code, metadata, details); +public sealed class PolymorfaAuthorizationException(string message, string? code = null, ResponseMetadata? metadata = null, JsonElement? details = null) : PolymorfaException(message, code, metadata, details); +public sealed class PolymorfaPaymentRequiredException(string message, string? code = null, ResponseMetadata? metadata = null, JsonElement? details = null) : PolymorfaException(message, code, metadata, details); +public sealed class PolymorfaValidationException(string message, string? code = null, ResponseMetadata? metadata = null, JsonElement? details = null) : PolymorfaException(message, code, metadata, details); +public sealed class PolymorfaNotFoundException(string message, string? code = null, ResponseMetadata? metadata = null, JsonElement? details = null) : PolymorfaException(message, code, metadata, details); +public sealed class PolymorfaConflictException(string message, string? code = null, ResponseMetadata? metadata = null, JsonElement? details = null) : PolymorfaException(message, code, metadata, details); +public sealed class PolymorfaRateLimitException(string message, string? code = null, ResponseMetadata? metadata = null, JsonElement? details = null) : PolymorfaException(message, code, metadata, details); +public sealed class PolymorfaServerException(string message, string? code = null, ResponseMetadata? metadata = null, JsonElement? details = null) : PolymorfaException(message, code, metadata, details); +public sealed class PolymorfaConnectionException(string message, ResponseMetadata? metadata = null) : PolymorfaException(message, "connection_error", metadata); +public sealed class PolymorfaTimeoutException(string message, ResponseMetadata? metadata = null) : PolymorfaException(message, "request_timeout", metadata); +public sealed class PolymorfaCancelledException(ResponseMetadata? metadata = null) : PolymorfaException("Request cancelled by caller.", "request_cancelled", metadata); +public sealed class WebhookSignatureException() : PolymorfaException("Webhook signature verification failed.", "invalid_webhook_signature"); diff --git a/packages/dotnet/src/Polymorfa.Sdk/EventPages.cs b/packages/dotnet/src/Polymorfa.Sdk/EventPages.cs new file mode 100644 index 00000000..67c8323f --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/EventPages.cs @@ -0,0 +1,53 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +public sealed record ListEventsParameters(string? Type = null, string? Since = null, string? Until = null, int? Limit = null, string? Cursor = null); +public sealed record ListIndexedEventsParameters(string AfterOffset, string? Type = null, int? Limit = null); +public sealed record RetrieveEventParameters(bool? IncludePayload = null); +public sealed record ReplayEventRequest(string WebhookId); +public sealed record IdempotencyReceipt(string Id, string Key, bool Replayed, string CreatedAt, string ExpiresAt); +public sealed record EventReplayReceipt(string EventId, string DeliveryId, string OperationId, IdempotencyReceipt Idempotency); +public sealed record IndexedEventMetadata([property: JsonRequired] bool HasMore, [property: JsonRequired] string HighWatermark, string? NextOffset = null, string? NextCursor = null); +public sealed record IndexedEventEnvelope([property: JsonRequired] IReadOnlyList Data, [property: JsonRequired] IndexedEventMetadata Page); +public sealed class IndexedEventPage : IAsyncEnumerable +{ + public IReadOnlyList Items { get; } + public string? NextOffset { get; } + public string HighWatermark { get; } + public bool HasMore { get; } + public ResponseMetadata Metadata { get; } + private readonly HttpTransport http; + private readonly string path; + private readonly ListIndexedEventsParameters parameters; + private readonly RequestOptions options; + private IndexedEventPage(HttpTransport http, string path, ListIndexedEventsParameters parameters, RequestOptions options, ApiResponse> response) + { + this.http = http; this.path = path; this.parameters = parameters; this.options = options; + Items = Array.AsReadOnly(response.Data.Data.ToArray()); NextOffset = response.Data.Page.NextOffset; HighWatermark = response.Data.Page.HighWatermark; HasMore = response.Data.Page.HasMore; Metadata = response.Metadata; + } + internal static async Task> LoadAsync(HttpTransport http, string path, ListIndexedEventsParameters parameters, RequestOptions options, CancellationToken? requestCancellation = null) + { + if (!Offset(parameters.AfterOffset, out var after) || after > long.MaxValue) throw new PolymorfaValidationException("afterOffset must be a non-negative decimal stream position.", "invalid_after_offset"); + var response = await http.RequestAsync>(HttpMethod.Get, path, null, requestCancellation is null ? options : options with { CancellationToken = requestCancellation.Value }, Resource.Query(parameters)).ConfigureAwait(false); + var page = response.Data.Page; + if (response.Data.Data is null || page is null || !Offset(page.HighWatermark, out var high) || page.NextOffset is not null && !Offset(page.NextOffset, out _) || page.HasMore && (page.NextOffset is null || !Offset(page.NextOffset, out var next) || next <= after || next > high) || !page.HasMore && page.NextOffset is not null) + throw new PolymorfaServerException("Polymorfa returned invalid indexed event metadata.", "invalid_response", response.Metadata); + return new(http, path, parameters, options, response); + } + private static bool Offset(string? value, out BigInteger number) { number = 0; return value is not null && Regex.IsMatch(value, "^(0|[1-9][0-9]*)$") && BigInteger.TryParse(value, out number); } + public async Task?> NextPageAsync(CancellationToken cancellationToken = default) + { + if (!HasMore) return null; + using var linked = CancellationTokenSource.CreateLinkedTokenSource(options.CancellationToken, cancellationToken); + return await LoadAsync(http, path, parameters with { AfterOffset = NextOffset! }, options, linked.Token).ConfigureAwait(false); + } + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + IndexedEventPage? page = this; + while (page is not null) { foreach (var item in page.Items) { cancellationToken.ThrowIfCancellationRequested(); yield return item; } page = await page.NextPageAsync(cancellationToken).ConfigureAwait(false); } + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/EventStream.cs b/packages/dotnet/src/Polymorfa.Sdk/EventStream.cs new file mode 100644 index 00000000..dfcadc76 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/EventStream.cs @@ -0,0 +1,199 @@ +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; + +namespace Polymorfa.Sdk; + +public sealed record EventStreamGap(string Reason, long MissedEvents, string? RequestedCursor); +public sealed record EventStreamItem(EventRecord Event, string StreamId, long Sequence, WebhookEvent? Webhook, string Cursor); +public sealed record EventStreamOptions +{ + public string? ProjectId { get; init; } + public IReadOnlyList? Types { get; init; } + public string? Since { get; init; } + public bool ManualAcknowledgement { get; init; } + public TimeSpan InitialReconnectDelay { get; init; } = TimeSpan.FromSeconds(1); + public TimeSpan MaximumReconnectDelay { get; init; } = TimeSpan.FromSeconds(30); + public TimeSpan? Timeout { get; init; } + public CancellationToken CancellationToken { get; init; } + public Action? OnGap { get; init; } + public Action? OnReconnect { get; init; } +} + +/// Resumable project events. Authentication and revoked cursors terminate the stream. +public sealed class EventStream : IAsyncEnumerable +{ + private readonly HttpTransport http; + private readonly string path; + private readonly EventStreamOptions options; + public string? Cursor { get; private set; } + internal EventStream(HttpTransport http, string path, EventStreamOptions options) + { + if (options.InitialReconnectDelay <= TimeSpan.Zero || options.MaximumReconnectDelay < options.InitialReconnectDelay) + throw new PolymorfaConfigurationException("Invalid event stream reconnect delays."); + this.http = http; this.path = path; this.options = options; Cursor = options.Since; + } + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + using var stop = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, options.CancellationToken); + var attempt = 0; + while (!stop.IsCancellationRequested) + { + Exception? failure = null; + await using (var source = ConnectAsync(stop.Token).GetAsyncEnumerator(stop.Token)) + { + while (true) + { + EventStreamItem? item = null; + try + { + if (!await source.MoveNextAsync().ConfigureAwait(false)) break; + item = source.Current; attempt = 0; + } + catch (Exception error) when (stop.IsCancellationRequested && error is OperationCanceledException or PolymorfaCancelledException) { break; } + catch (Exception error) when (!Terminal(error)) { failure = error; if (error is StreamReconnect { ResetBackoff: true }) attempt = 0; break; } + yield return item; + } + } + if (stop.IsCancellationRequested) yield break; + var ceiling = Math.Min(options.MaximumReconnectDelay.TotalMilliseconds, options.InitialReconnectDelay.TotalMilliseconds * Math.Pow(2, Math.Min(attempt++, 20))); + var delay = TimeSpan.FromMilliseconds(ceiling * (0.5 + Random.Shared.NextDouble() * 0.5)); + if (failure is PolymorfaException { Metadata: { } metadata } && metadata.Headers.TryGetValue("retry-after", out var retry) && double.TryParse(retry, System.Globalization.CultureInfo.InvariantCulture, out var seconds) && seconds >= 0 && double.IsFinite(seconds)) + delay = TimeSpan.FromSeconds(Math.Min(seconds, options.MaximumReconnectDelay.TotalSeconds)); + else if (failure is PolymorfaException { Metadata: { } dated } && dated.Headers.TryGetValue("retry-after", out var date) && DateTimeOffset.TryParse(date, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal, out var retryAt)) + delay = TimeSpan.FromMilliseconds(Math.Clamp((retryAt - DateTimeOffset.UtcNow).TotalMilliseconds, 0, options.MaximumReconnectDelay.TotalMilliseconds)); + options.OnReconnect?.Invoke(failure, delay); + try { await Task.Delay(delay, stop.Token).ConfigureAwait(false); } + catch (OperationCanceledException) { yield break; } + } + } + private static bool Terminal(Exception error) => error is PolymorfaAuthenticationException or PolymorfaAuthorizationException or PolymorfaNotFoundException or PolymorfaValidationException || error is PolymorfaException { Status: 410 }; + private async IAsyncEnumerable ConnectAsync([EnumeratorCancellation] CancellationToken cancellation) + { + var query = new List>(); + if (options.Types is { Count: > 0 }) query.Add(new("types", string.Join(',', options.Types))); + if (options.ManualAcknowledgement) query.Add(new("ack", "manual")); + var headers = new Dictionary(); + if (Cursor is not null) headers["last-event-id"] = Cursor; + using var opened = await http.OpenAsync(HttpMethod.Get, path, null, new RequestOptions { Headers = headers, Timeout = options.Timeout, MaxNetworkRetries = 0, CancellationToken = cancellation }, "text/event-stream", query).ConfigureAwait(false); + if (opened.Response.Content.Headers.ContentType?.MediaType != "text/event-stream") throw new PolymorfaServerException("Expected an event stream.", "invalid_stream", opened.Metadata); + await using var stream = await opened.Response.Content.ReadAsStreamAsync(cancellation).ConfigureAwait(false); + var lines = new EventSseLines(stream); + var data = new StringBuilder(); + var heartbeat = TimeSpan.FromSeconds(30); + using var watchdog = CancellationTokenSource.CreateLinkedTokenSource(cancellation); + while (true) + { + watchdog.CancelAfter(heartbeat); + var line = await lines.ReadAsync(watchdog.Token).ConfigureAwait(false); + if (line is null) yield break; + if (line.Length != 0) + { + if (line.StartsWith("data:", StringComparison.Ordinal)) { if (data.Length != 0) data.Append('\n'); data.Append(line[5..].TrimStart(' ')); } + continue; + } + if (data.Length == 0) continue; + using var document = JsonDocument.Parse(data.ToString()); data.Clear(); + var frame = document.RootElement; + if (!frame.TryGetProperty("type", out var type)) continue; + switch (type.GetString()) + { + case "ready": + if (frame.TryGetProperty("heartbeatIntervalMs", out var interval) && interval.TryGetInt64(out var ms) && ms > 0 && ms < uint.MaxValue / 2) heartbeat = TimeSpan.FromMilliseconds(ms * 2); + break; + case "event": + var record = frame.GetProperty("event").Deserialize(HttpTransport.Json) ?? throw new PolymorfaServerException("Invalid event stream envelope.", "invalid_stream"); + var cursor = frame.GetProperty("cursor").GetString() ?? throw new PolymorfaServerException("Missing stream cursor.", "invalid_stream"); + WebhookEvent? webhook = null; + if (record.Payload is { } payload) + { + if (payload.Encoding != "base64") throw new PolymorfaServerException("Unsupported event payload encoding.", "invalid_stream"); + webhook = Webhooks.ParseVerifiedEvent(Convert.FromBase64String(payload.Data)); + } + var item = new EventStreamItem(record, frame.GetProperty("streamId").GetString()!, frame.GetProperty("sequence").GetInt64(), webhook, cursor); + Cursor = cursor; + yield return item; + break; + case "checkpoint": if (frame.TryGetProperty("cursor", out var checkpoint) && checkpoint.ValueKind == JsonValueKind.String) Cursor = checkpoint.GetString(); break; + case "gap": + if (frame.GetProperty("reason").GetString() != "retention_exceeded") throw new PolymorfaConnectionException("Event stream reported a recoverable gap."); + options.OnGap?.Invoke(new("retention_exceeded", frame.TryGetProperty("missedEvents", out var missed) ? missed.GetInt64() : 0, frame.TryGetProperty("requestedCursor", out var requested) ? requested.GetString() : null)); + break; + case "revoked": throw new PolymorfaAuthorizationException("Event stream access was revoked.", "stream_revoked"); + case "expiry": throw new StreamReconnect(true); + case "dropped": throw new StreamReconnect(false); + } + } + } +} + +// This parser belongs only to project event streams; Signal retains its own policy. +internal sealed class EventSseLines(Stream stream) +{ + private readonly byte[] buffer = new byte[4096]; + private readonly MemoryStream line = new(); + private static readonly UTF8Encoding Utf8 = new(false, true); + private int position; + private int length; + private int frameBytes; + private bool skipLineFeed; + private bool firstLine = true; + internal async ValueTask ReadAsync(CancellationToken cancellation) + { + while (true) + { + if (position == length) + { + length = await stream.ReadAsync(buffer.AsMemory(), cancellation).ConfigureAwait(false); position = 0; + if (length == 0) return line.Length == 0 ? null : TakeLine(); + } + var character = buffer[position++]; + if (skipLineFeed) { skipLineFeed = false; if (character == 10) continue; } + if (++frameBytes > 4 * 1024 * 1024) throw new PolymorfaServerException("Event stream frame exceeds its size limit.", "invalid_stream"); + if (character is 13 or 10) + { + skipLineFeed = character == 13; + var value = TakeLine(); + if (value.Length == 0) frameBytes = 0; + return value; + } + line.WriteByte(character); + } + } + private string TakeLine() + { + var value = Utf8.GetString(line.GetBuffer(), 0, (int)line.Length); line.SetLength(0); + if (firstLine) { firstLine = false; value = value.TrimStart('\ufeff'); } + return value; + } +} + +internal sealed class StreamReconnect(bool resetBackoff) : PolymorfaException("Event stream connection ended; resume at the saved cursor.", "stream_reconnect") +{ + internal bool ResetBackoff { get; } = resetBackoff; +} +internal sealed class BoundedSseLines(StreamReader reader) +{ + internal bool IncompleteLine { get; private set; } + private readonly char[] buffer = new char[4096]; + private int position; + private int length; + private bool skipLineFeed; + private readonly StringBuilder line = new(); + internal async ValueTask ReadAsync(CancellationToken cancellation) + { + while (true) + { + if (position == length) + { + length = await reader.ReadAsync(buffer.AsMemory(), cancellation).ConfigureAwait(false); position = 0; + if (length == 0) { if (line.Length == 0) return null; IncompleteLine = true; var tail = line.ToString(); line.Clear(); return tail; } + } + var character = buffer[position++]; + if (skipLineFeed) { skipLineFeed = false; if (character == '\n') continue; } + if (character is '\r' or '\n') { skipLineFeed = character == '\r'; var value = line.ToString(); line.Clear(); return value; } + if (line.Length == 4 * 1024 * 1024) throw new PolymorfaServerException("Event stream line exceeds its size limit.", "invalid_stream"); + line.Append(character); + } + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/EventStreamSource.cs b/packages/dotnet/src/Polymorfa.Sdk/EventStreamSource.cs new file mode 100644 index 00000000..04c36974 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/EventStreamSource.cs @@ -0,0 +1,61 @@ +namespace Polymorfa.Sdk; + +public sealed record EventSourceMetadata(string Cursor); +public sealed record EventSourceSubscriptionOptions +{ + public string? Cursor { get; init; } + public CancellationToken CancellationToken { get; init; } + public Action? OnError { get; init; } +} + +/// Feeds decoded webhook bodies to an application-owned event store. +public sealed class EventStreamSource(Func> open, EventStreamOptions? options = null) +{ + public EventSourceSubscription Subscribe(Action, EventSourceMetadata> listener, EventSourceSubscriptionOptions? subscriptionOptions = null) + { + ArgumentNullException.ThrowIfNull(listener); + return new(open, options ?? new(), listener, subscriptionOptions ?? new()); + } +} + +/// Dispose to unsubscribe; await Completion or DisposeAsync to await stream cleanup. +public sealed class EventSourceSubscription : IDisposable, IAsyncDisposable +{ + private readonly CancellationTokenSource stop; + private readonly object gate = new(); + private bool finished; + public Task Completion { get; } + internal EventSourceSubscription(Func> open, EventStreamOptions options, Action, EventSourceMetadata> listener, EventSourceSubscriptionOptions subscription) + { + stop = CancellationTokenSource.CreateLinkedTokenSource(options.CancellationToken, subscription.CancellationToken); + Completion = PumpAsync(open, options with { Since = subscription.Cursor, CancellationToken = stop.Token }, listener, subscription.OnError); + } + private async Task PumpAsync(Func> open, EventStreamOptions options, Action, EventSourceMetadata> listener, Action? onError) + { + try + { + await foreach (var item in open(options).WithCancellation(stop.Token).ConfigureAwait(false)) + { + if (stop.IsCancellationRequested) break; + if (item.Webhook is not null) listener(Array.AsReadOnly(new[] { item.Webhook }), new(item.Cursor)); + } + } + catch (Exception error) + { + if (!stop.IsCancellationRequested) onError?.Invoke(error); + } + finally + { + lock (gate) { finished = true; stop.Dispose(); } + } + } + public void Dispose() + { + lock (gate) { if (!finished) stop.Cancel(); } + } + public async ValueTask DisposeAsync() + { + Dispose(); + await Completion.ConfigureAwait(false); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/ExactLengthAudioContent.cs b/packages/dotnet/src/Polymorfa.Sdk/ExactLengthAudioContent.cs new file mode 100644 index 00000000..f134a9ab --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/ExactLengthAudioContent.cs @@ -0,0 +1,30 @@ +using System.Buffers; +using System.Net; + +namespace Polymorfa.Sdk; + +/// Streams exactly the declared bytes. Leaves the caller's stream open and never retries storage writes. +internal sealed class ExactLengthAudioContent(Stream source, long size) : HttpContent +{ + internal PolymorfaValidationException? Failure { get; private set; } + protected override bool TryComputeLength(out long length) { length = size; return true; } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => SerializeToStreamAsync(stream, context, CancellationToken.None); + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + var buffer = ArrayPool.Shared.Rent(65536); + try + { + long sent = 0; + while (sent < size) + { + var count = await source.ReadAsync(buffer.AsMemory(0, (int)Math.Min(buffer.Length, size - sent)), cancellationToken).ConfigureAwait(false); + if (count == 0) throw Mismatch(); + await stream.WriteAsync(buffer.AsMemory(0, count), cancellationToken).ConfigureAwait(false); + sent += count; + } + if (await source.ReadAsync(buffer.AsMemory(0, 1), cancellationToken).ConfigureAwait(false) != 0) throw Mismatch(); + } + finally { ArrayPool.Shared.Return(buffer, clearArray: true); } + } + private PolymorfaValidationException Mismatch() => Failure = new("The upload size does not match the audio bytes.", "invalid_parameter"); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/ExistingContractCodec.cs b/packages/dotnet/src/Polymorfa.Sdk/ExistingContractCodec.cs new file mode 100644 index 00000000..b5f06033 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/ExistingContractCodec.cs @@ -0,0 +1,114 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +// Existing endpoints have distinct envelopes and optional fields. Keep their +// policy separate from the stricter, non-null supplement data contract. +internal static class ExistingContractCodec +{ + internal static RequestOptions Options(RequestOptions? options, bool intent = false) + { + if (options?.ApiVersion is { Length: 0 }) throw new PolymorfaConfigurationException("Invalid apiVersion."); + return (options ?? new()) with { ApiVersion = options?.ApiVersion ?? (intent ? WhatsAppMediaResource.CustomerOwnedApiVersion : null) }; + } + internal static CampaignCapturedBody Capture(object body) => new(CampaignContractCodec.Encode(CampaignContractValidation.Capture(body))); + internal static async Task> Request(HttpTransport http, HttpMethod method, string path, object? body = null, RequestOptions? options = null, IReadOnlyList>? query = null, bool nullableData = false, int status = 200, bool platformRecipients = false) + { + var response = await http.RequestAsync(method, path, body, Options(options), query).ConfigureAwait(false); + return Decode(response, nullableData, status, platformRecipients); + } + internal static ApiResponse Decode(ApiResponse response, bool nullableData = false, int status = 200, bool platformRecipients = false) + { + try + { + CheckMetadata(response.Metadata, status); + Validate(response.Data, typeof(T), false, nullableData); + if (platformRecipients) + foreach (var row in response.Data.GetProperty("data").EnumerateArray()) + if (row.GetProperty("nextMessageAt") is { ValueKind: JsonValueKind.Number } next && next.GetDouble() != Math.Truncate(next.GetDouble())) throw new JsonException(); + return new(response.Data.Deserialize(HttpTransport.Json) ?? throw new JsonException(), response.Metadata); + } + catch (Exception error) when (error is JsonException or InvalidOperationException or KeyNotFoundException or FormatException or OverflowException) + { + throw new PolymorfaServerException("Invalid API response structure.", "invalid_response", response.Metadata); + } + } + internal static void CheckMetadata(ResponseMetadata metadata, int status = 200) + { + if (metadata.Status != status || !string.Equals(metadata.Headers.GetValueOrDefault("content-type")?.Split(';')[0].Trim(), "application/json", StringComparison.OrdinalIgnoreCase)) throw new JsonException(); + } + private static bool Optional(Type type, string name) + { + if (typeof(Campaign).IsAssignableFrom(type)) return (name is "composerBlueprint" or "messages" or "audienceRef" or "senderConfig" or "complianceConfig" or "variants" or "variantStrategy" or "messageVariations" or "variableMapping" or "frequencyCap" or "drip" or "enrollmentOpen" or "pauseReason" or "segmentId" or "segmentSnapshot") || name == "operationId" && type == typeof(CampaignStopOperation); + if (typeof(CampaignAnalytics).IsAssignableFrom(type)) return name is "frequencyCap" or "drip"; + if (type == typeof(AudienceSegmentGroup)) return name == "negate"; + if (type == typeof(CampaignFollowUp)) return name is "delaySeconds" or "expirySeconds"; + if (type == typeof(CampaignVariableBinding)) return name is "source" or "fallback"; + if (type == typeof(CampaignVariantStrategy)) return name == "testSlicePercent"; + if (type == typeof(Session)) return name is "externalId" or "statusReason" or "signalMode" or "configuration" or "newChatCapping"; + if (type == typeof(HistorySyncPolicy)) return name == "delivery"; + if (type == typeof(ConfigurationSources)) return name == "historySync.delivery"; + if (type == typeof(SessionConfigurationView)) return name is "historyConsent" or "application"; + if (type == typeof(SessionConfigurationOverrides)) return true; + if (type == typeof(HistorySyncOverride) || type == typeof(ObservationOverride) || type == typeof(HmsConfiguration)) return true; + if (typeof(WhatsAppMetrics).IsAssignableFrom(type)) return name is "recipientActivity" or "responseQueue"; + if (type == typeof(WhatsAppConversationGroup)) return name is "recipientCountry" or "recipientDeviceCount"; + return false; + } + private static bool OptionalNull(Type type, string name) => typeof(Campaign).IsAssignableFrom(type) || typeof(CampaignAnalytics).IsAssignableFrom(type) && name == "drip" || typeof(WhatsAppMetrics).IsAssignableFrom(type) && name == "responseQueue" || type == typeof(Session) && name is "externalId" or "statusReason" or "configuration" or "newChatCapping"; + private static void Validate(JsonElement value, Type type, bool nullable, bool nullableData = false) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(PatchValue<>)) type = type.GetGenericArguments()[0]; + type = Nullable.GetUnderlyingType(type) ?? type; + if (value.ValueKind == JsonValueKind.Null) { if (!nullable) throw new JsonException(); return; } + if (type == typeof(JsonElement)) return; + if (type == typeof(CampaignExperimentOutcome)) + { + type = value.GetProperty("state").GetString() switch { "promoted" => typeof(CampaignPromoted), "inconclusive" => typeof(CampaignInconclusive), _ => throw new JsonException() }; + } + if (type == typeof(CampaignFollowUpMessage)) type = value.GetProperty("type").GetString() switch { "text" => typeof(CampaignFollowUpText), "template" => typeof(CampaignFollowUpTemplate), _ => throw new JsonException() }; + if (type == typeof(string)) { if (value.ValueKind != JsonValueKind.String) throw new JsonException(); return; } + if (type == typeof(bool)) { if (value.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) throw new JsonException(); return; } + if (type == typeof(long) || type == typeof(int)) { if (value.ValueKind != JsonValueKind.Number || !value.TryGetInt64(out _) || type == typeof(int) && !value.TryGetInt32(out _)) throw new JsonException(); return; } + if (type == typeof(double) || type == typeof(decimal)) { if (value.ValueKind != JsonValueKind.Number || !value.TryGetDouble(out var number) || !double.IsFinite(number)) throw new JsonException(); return; } + if (type.IsEnum) { if (value.ValueKind != JsonValueKind.String) throw new JsonException(); var parsed = JsonSerializer.Deserialize(value.GetRawText(), type, HttpTransport.Json); if (JsonSerializer.SerializeToElement(parsed, type, HttpTransport.Json).GetString() != value.GetString()) throw new JsonException(); return; } + if (type == typeof(CampaignVariableMapping)) + { + if (value.ValueKind != JsonValueKind.Object) throw new JsonException(); + foreach (var entry in value.EnumerateObject()) Validate(entry.Value, typeof(CampaignVariableBinding), false); + return; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)) + { + if (value.ValueKind != JsonValueKind.Array) throw new JsonException(); + foreach (var item in value.EnumerateArray()) Validate(item, type.GetGenericArguments()[0], false); + return; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) + { + if (value.ValueKind != JsonValueKind.Object) throw new JsonException(); + foreach (var item in value.EnumerateObject()) Validate(item.Value, type.GetGenericArguments()[1], false); + return; + } + if (value.ValueKind != JsonValueKind.Object) throw new JsonException(); + var context = new NullabilityInfoContext(); + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (property.GetCustomAttribute() is not null) continue; + var name = property.GetCustomAttribute()?.Name ?? JsonNamingPolicy.CamelCase.ConvertName(property.Name); + var optional = Optional(type, name); + if (!value.TryGetProperty(name, out var item)) { if (!optional) throw new JsonException(); continue; } + var envelope = name == "data" && type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(DataEnvelope<>) || type.GetGenericTypeDefinition() == typeof(SuccessEnvelope<>)); + var allowsNull = envelope ? nullableData : (context.Create(property).ReadState == NullabilityState.Nullable || Nullable.GetUnderlyingType(property.PropertyType) is not null) && (!optional || OptionalNull(type, name)); + Validate(item, property.PropertyType, allowsNull); + if (item.ValueKind == JsonValueKind.Number && (name.EndsWith("Count", StringComparison.Ordinal) || name is "messagesSent" or "attempts" || type == typeof(CampaignFollowUpReport) && name != "nextDueAt") && item.GetDouble() < 0) throw new JsonException(); + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(SuccessEnvelope<>) && !value.GetProperty("success").GetBoolean()) throw new JsonException(); + if (type == typeof(CampaignBlueprint) && value.GetProperty("version").GetInt32() != 2) throw new JsonException(); + if (type == typeof(UnlinkTestingPhoneDeviceResponse) && !value.GetProperty("unlinked").GetBoolean()) throw new JsonException(); + if (type == typeof(GroupCapabilities) && value.GetProperty("status").GetString() is not ("synced" or "unknown")) throw new JsonException(); + if (type == typeof(GroupCapability) && (value.GetProperty("kind").GetString() != "feature" || value.GetProperty("unit").ValueKind != JsonValueKind.Null || value.GetProperty("source") is { ValueKind: not JsonValueKind.Null } source && source.GetString() is not ("server" or "client_default"))) throw new JsonException(); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Flows.cs b/packages/dotnet/src/Polymorfa.Sdk/Flows.cs new file mode 100644 index 00000000..e2eede7f --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Flows.cs @@ -0,0 +1,92 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +public sealed record FlowValidationPointer(string? Path = null, int? LineStart = null, int? LineEnd = null, int? ColumnStart = null, int? ColumnEnd = null); +public sealed record FlowValidationIssue(string? Error = null, string? ErrorType = null, string? Message = null, IReadOnlyList? Pointers = null, int? LineStart = null, int? LineEnd = null, int? ColumnStart = null, int? ColumnEnd = null); +public sealed record FlowNumberLink(string Session, string WabaId, string MetaFlowId, string Status, IReadOnlyList Categories, IReadOnlyList ValidationErrors, string UploadState, long LastSyncedAt, string? SessionId = null, string? PreviewUrl = null, long? PreviewExpiresAt = null, string? DefinitionDigest = null, bool? Simulated = null); +public record FlowSummary(string Id, string Name, string Status, string Version, int ScreenCount, IReadOnlyList MetaLinks, long CreatedAt, long UpdatedAt); +public sealed record FlowDraft(string Id, string Name, string Status, string Version, int ScreenCount, IReadOnlyList MetaLinks, long CreatedAt, long UpdatedAt, IReadOnlyDictionary Definition) : FlowSummary(Id, Name, Status, Version, ScreenCount, MetaLinks, CreatedAt, UpdatedAt); +public sealed record CreateFlowRequest(string Name, IReadOnlyDictionary Definition, string? DraftId = null); +public sealed record UpdateFlowRequest(long ExpectedUpdatedAt, string? Name = null, string? Status = null, IReadOnlyDictionary? Definition = null); +public sealed record FlowProviderRequest(string SessionId, IReadOnlyList? Categories = null, string? RequestId = null); +public sealed record FlowProviderOperation(string Id, string? RequestId, string FlowId, string FlowName, string SessionId, string Session, string Action, string State, string? Resolution, string? WabaId, string? MetaFlowId, string? DefinitionDigest, string? ProviderStatus, string? ErrorCode, int? ProviderCode, int? ProviderSubcode, long CreatedAt, long UpdatedAt, long? CompletedAt); +public sealed record FlowProviderResult(FlowProviderOperation? Operation, FlowDraft Flow); +public sealed record FlowEndpoint(string Id, string OrgId, string ProjectId, string FlowId, string SessionId, string Mode, string? Url, string? FunctionId, string? DeploymentId, bool Enabled, long Revision, string EndpointUri, long CreatedAt, long UpdatedAt); +public sealed record ManagedFlowEncryptionKey(string Id, string State, string Fingerprint, string PublicKey, string? ErrorCode, long CreatedAt, long? ActivatedAt, long? RetireAfter); +public record FlowEncryptionCustody(string Custody, string? ActiveKeyId, IReadOnlyList Keys); +public sealed record FlowEncryptionKeyRotation(string Custody, string? ActiveKeyId, IReadOnlyList Keys, ManagedFlowEncryptionKey Key) : FlowEncryptionCustody(Custody, ActiveKeyId, Keys); +public sealed record FlowEndpointState(FlowEndpoint? Endpoint, FlowEncryptionCustody Encryption); +public sealed record FlowEndpointSetResult(FlowEndpoint Endpoint, FlowEncryptionCustody Encryption, string? SigningSecret = null) +{ + public override string ToString() => $"FlowEndpointSetResult {{ EndpointId = {Endpoint.Id}, SigningSecret = [REDACTED] }}"; +} +public abstract record SetFlowEndpointRequest(string SessionId, bool? Enabled = null, long? ExpectedRevision = null) { public abstract string Mode { get; } } +public sealed record ForwardFlowEndpointRequest(string SessionId, string Url, bool? RotateSigningSecret = null, bool? Enabled = null, long? ExpectedRevision = null) : SetFlowEndpointRequest(SessionId, Enabled, ExpectedRevision) { public override string Mode => "forward"; } +public sealed record DirectFlowEndpointRequest(string SessionId, string Url, bool? Enabled = null, long? ExpectedRevision = null) : SetFlowEndpointRequest(SessionId, Enabled, ExpectedRevision) { public override string Mode => "direct"; } +public sealed record FunctionFlowEndpointRequest(string SessionId, string FunctionId, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] PatchValue DeploymentId = default, bool? Enabled = null, long? ExpectedRevision = null) : SetFlowEndpointRequest(SessionId, Enabled, ExpectedRevision) { public override string Mode => "function"; } +public sealed record FlowNumberRequest(string SessionId); +public sealed record ListFlowEndpointReceiptsParameters(string? SessionId = null, int? Limit = null); +public sealed record FlowEndpointReceipt(string Id, string FlowId, string EndpointId, string SessionId, string Mode, string? Action, string Outcome, int? HttpStatus, string? ErrorCode, string? KeyId, string? FunctionInvocationId, double? DurationMs, long CreatedAt, long? CompletedAt); +public sealed record FlowOk(bool Ok); +public sealed class Flows : Resource +{ + private readonly string projectId; + internal Flows(HttpTransport http, string projectId) : base(http) => this.projectId = projectId; + private static string Path(string flowId) => string.IsNullOrWhiteSpace(flowId) ? throw new PolymorfaConfigurationException("A non-empty flowId is required.") : "/platform/flows/" + E(flowId); + private async Task> Request(HttpMethod method, string path, object? body, RequestOptions? options) + { + IReadOnlyList>? query = null; + if (method == HttpMethod.Get || method == HttpMethod.Delete) { query = Query(body).Append(new("projectId", projectId)).ToArray(); body = null; } + else + { + var payload = body is null ? new Dictionary() : JsonSerializer.SerializeToElement(body, body.GetType(), HttpTransport.Json).EnumerateObject().ToDictionary(p => p.Name, p => p.Value); + payload["projectId"] = JsonSerializer.SerializeToElement(projectId); body = payload; + } + var response = await Http.RequestAsync>(method, path, body, method == HttpMethod.Get ? options : (options ?? new()) with { MaxNetworkRetries = 0 }, query).ConfigureAwait(false); + return new(response.Data.Data, response.Metadata); + } + public Task>> ListAsync(RequestOptions? options = null) => Request>(HttpMethod.Get, "/platform/flows", null, options); + public Task> CreateAsync(CreateFlowRequest body, RequestOptions? options = null) => Request(HttpMethod.Post, "/platform/flows", body, options); + public Task> RetrieveAsync(string flowId, RequestOptions? options = null) + { + if (!Guid.TryParseExact(flowId, "D", out _)) throw new PolymorfaValidationException("flowId must be a valid UUID.", "invalid_parameter"); + return Request(HttpMethod.Get, Path(flowId), null, options); + } + public Task> UpdateAsync(string flowId, UpdateFlowRequest body, RequestOptions? options = null) => Request(HttpMethod.Patch, Path(flowId), body, options); + public Task> DeleteAsync(string flowId, RequestOptions? options = null) => Request(HttpMethod.Delete, Path(flowId), null, options); + public Task> UploadAsync(string flowId, FlowProviderRequest body, RequestOptions? options = null) => Request(HttpMethod.Post, Path(flowId) + "/upload", body, options); + public Task> PublishAsync(string flowId, FlowProviderRequest body, RequestOptions? options = null) => Request(HttpMethod.Post, Path(flowId) + "/publish", body, options); + public Task> DeprecateAsync(string flowId, FlowProviderRequest body, RequestOptions? options = null) => Request(HttpMethod.Post, Path(flowId) + "/deprecate", body, options); + public Task> DiscardAsync(string flowId, FlowProviderRequest body, RequestOptions? options = null) => Request(HttpMethod.Post, Path(flowId) + "/discard", body, options); + public Task> SyncAsync(string flowId, FlowProviderRequest body, RequestOptions? options = null) => Request(HttpMethod.Post, Path(flowId) + "/sync", body, options); + public Task>> ReceiptsAsync(string flowId, RequestOptions? options = null) => Request>(HttpMethod.Get, Path(flowId) + "/receipts", null, options); + public Task> EndpointAsync(string flowId, FlowNumberRequest parameters, RequestOptions? options = null) { ValidateNumber(parameters.SessionId); return Request(HttpMethod.Get, Path(flowId) + "/endpoint", parameters, options); } + public Task> SetEndpointAsync(string flowId, SetFlowEndpointRequest body, RequestOptions? options = null) + { + ValidateNumber(body.SessionId); + if (body.ExpectedRevision is < 1) throw new PolymorfaValidationException("expectedRevision must be positive.", "invalid_parameter"); + switch (body) + { + case ForwardFlowEndpointRequest forward: ValidateEndpointUrl(forward.Url); break; + case DirectFlowEndpointRequest direct: ValidateEndpointUrl(direct.Url); break; + case FunctionFlowEndpointRequest function when !string.IsNullOrWhiteSpace(function.FunctionId): break; + default: throw new PolymorfaValidationException("Invalid Flow endpoint mode or functionId.", "invalid_parameter"); + } + return Request(HttpMethod.Put, Path(flowId) + "/endpoint", body, options); + } + public Task> DeleteEndpointAsync(string flowId, FlowNumberRequest parameters, RequestOptions? options = null) { ValidateNumber(parameters.SessionId); return Request(HttpMethod.Delete, Path(flowId) + "/endpoint", parameters, options); } + public Task>> EndpointReceiptsAsync(string flowId, ListFlowEndpointReceiptsParameters? parameters = null, RequestOptions? options = null) + { + if (parameters?.Limit is < 1 or > 100) throw new PolymorfaValidationException("limit must be from 1 to 100.", "invalid_parameter"); + return Request>(HttpMethod.Get, Path(flowId) + "/endpoint/receipts", parameters, options); + } + public Task> EncryptionKeyAsync(FlowNumberRequest parameters, RequestOptions? options = null) { ValidateNumber(parameters.SessionId); return Request(HttpMethod.Get, "/platform/flow-encryption-keys", parameters, options); } + public Task> RotateEncryptionKeyAsync(FlowNumberRequest parameters, RequestOptions? options = null) { ValidateNumber(parameters.SessionId); return Request(HttpMethod.Post, "/platform/flow-encryption-keys/rotate", parameters, options); } + private static void ValidateNumber(string sessionId) { if (string.IsNullOrWhiteSpace(sessionId)) throw new PolymorfaValidationException("sessionId is required.", "invalid_parameter"); } + private static void ValidateEndpointUrl(string value) + { + if (value.Length > 2048 || !Uri.TryCreate(value, UriKind.Absolute, out var url) || url.Scheme != "https" || url.UserInfo.Length > 0 || url.Fragment.Length > 0) throw new PolymorfaValidationException("Flow endpoint must be an HTTPS URL without user information or fragment.", "invalid_parameter"); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Functions.cs b/packages/dotnet/src/Polymorfa.Sdk/Functions.cs new file mode 100644 index 00000000..a14fe2f5 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Functions.cs @@ -0,0 +1,80 @@ +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +public sealed record FunctionDefinition(string Id, string ProjectId, string Name, bool Enabled, long Revision, string? ActiveDeploymentId, string CreatedAt, string UpdatedAt); +public sealed record FunctionPage(IReadOnlyList Items, string? NextCursor); +public sealed record ListFunctionsParameters(int? Limit = null, string? Before = null); +public sealed record CreateFunctionInput(string Name, string? FunctionId = null); +public sealed record UpdateFunctionInput(long ExpectedRevision, string? Name = null, bool? Enabled = null); +public sealed record CreateFunctionDeploymentInput(string DeploymentId, string Source, string Language, string Region, string CompatibilityDate, IReadOnlyList? SecretVersionIds = null, IReadOnlyList? EgressOrigins = null); +public record FunctionDeploymentSummary(string Id, string FunctionId, string Language, string Region, string CompatibilityDate, string Sha256, IReadOnlyList SecretVersionIds, IReadOnlyList EgressOrigins, string CreatedAt); +public sealed record FunctionDeployment(string Id, string FunctionId, string Source, string Language, string Region, string CompatibilityDate, string Sha256, IReadOnlyList SecretVersionIds, IReadOnlyList EgressOrigins, string CreatedAt); +public sealed record PromoteFunctionDeploymentInput(string DeploymentId, long ExpectedRevision); +public sealed record FunctionSecretVersion(string Id, string Name, string CreatedAt, string? RevokedAt); +public sealed record CreateFunctionSecretInput(string Name, string Value) { public override string ToString() => $"CreateFunctionSecretInput(name={Name}, value=redacted)"; } +public sealed record FunctionRequest(string Method, string Url, IReadOnlyDictionary Headers, string BodyBase64); +public sealed record FunctionResponse(int Status, IReadOnlyDictionary Headers, string BodyBase64); +public sealed record FunctionInvocation(string Id, string FunctionId, string DeploymentId, string Outcome, string Trigger, string? ErrorCode, long? DurationMs, long? ResponseBytes, int Attempt, string CreatedAt, string? CompletedAt); +public sealed record CreateFunctionInvocationInput(FunctionRequest Request, string? DeploymentId = null, string? Trigger = null); +public sealed record FunctionInvocationResult(FunctionInvocation Receipt, bool Replayed, bool ResponseRetained, bool Retryable, FunctionResponse? Response = null); +public sealed record FunctionMutationResult(bool Ok); +internal sealed class FunctionTransport(HttpTransport http, string projectId) +{ + internal static string Id(string id) => Regex.IsMatch(id, "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", RegexOptions.CultureInvariant) ? id : throw Invalid("A canonical Functions UUID is required."); + internal static void Revision(long revision) { if (revision < 1 || revision > 9007199254740991L) throw Invalid("A positive Function revision is required."); } + internal static PolymorfaValidationException Invalid(string message) => new(message, "invalid_function_input"); + internal async Task> Request(HttpMethod method, string suffix, object? input, RequestOptions? options) + { + var fields = input is null ? new Dictionary() : JsonSerializer.SerializeToElement(input, input.GetType(), HttpTransport.Json).EnumerateObject().ToDictionary(p => p.Name, p => p.Value.Clone()); + fields.Add("projectId", JsonSerializer.SerializeToElement(Id(projectId))); + options ??= new(); if (method != HttpMethod.Get) options = options with { MaxNetworkRetries = 0 }; + var query = method == HttpMethod.Get || method == HttpMethod.Delete; + var response = await http.RequestAsync>(method, "/platform/functions" + suffix, query ? null : fields, options, query ? Resource.Query(fields) : null).ConfigureAwait(false); + return new(response.Data.Data, response.Metadata); + } +} +public sealed class Functions +{ + private readonly FunctionTransport api; + public FunctionDeployments Deployments { get; } + public FunctionSecrets Secrets { get; } + public FunctionInvocations Invocations { get; } + internal Functions(HttpTransport http, string projectId) { api = new(http, projectId); Deployments = new(api); Secrets = new(api); Invocations = new(api); } + public Task>> ListAsync(ListFunctionsParameters? parameters = null, RequestOptions? options = null) => api.Request>(HttpMethod.Get, "", parameters, options); + public Task> CreateAsync(CreateFunctionInput input, RequestOptions? options = null) { if (input.FunctionId is not null) FunctionTransport.Id(input.FunctionId); return api.Request(HttpMethod.Post, "", input, options); } + public Task> RetrieveAsync(string id, RequestOptions? options = null) => api.Request(HttpMethod.Get, "/" + FunctionTransport.Id(id), null, options); + public Task> UpdateAsync(string id, UpdateFunctionInput input, RequestOptions? options = null) { FunctionTransport.Revision(input.ExpectedRevision); return api.Request(HttpMethod.Patch, "/" + FunctionTransport.Id(id), input, options); } + public Task> DeleteAsync(string id, long expectedRevision, RequestOptions? options = null) { FunctionTransport.Revision(expectedRevision); return api.Request(HttpMethod.Delete, "/" + FunctionTransport.Id(id), new { expectedRevision }, options); } +} +public sealed class FunctionDeployments +{ + private readonly FunctionTransport api; + internal FunctionDeployments(FunctionTransport api) => this.api = api; + public Task>> ListAsync(string functionId, ListFunctionsParameters? parameters = null, RequestOptions? options = null) => api.Request>(HttpMethod.Get, "/" + FunctionTransport.Id(functionId) + "/deployments", parameters, options); + public Task> CreateAsync(string functionId, CreateFunctionDeploymentInput input, RequestOptions? options = null) { FunctionTransport.Id(input.DeploymentId); return api.Request(HttpMethod.Post, "/" + FunctionTransport.Id(functionId) + "/deployments", input, options); } + public Task> RetrieveAsync(string functionId, string deploymentId, RequestOptions? options = null) => api.Request(HttpMethod.Get, "/" + FunctionTransport.Id(functionId) + "/deployments/" + FunctionTransport.Id(deploymentId), null, options); + public Task> PromoteAsync(string functionId, PromoteFunctionDeploymentInput input, RequestOptions? options = null) { FunctionTransport.Id(input.DeploymentId); FunctionTransport.Revision(input.ExpectedRevision); return api.Request(HttpMethod.Put, "/" + FunctionTransport.Id(functionId) + "/promotion", input, options); } +} +public sealed class FunctionSecrets +{ + private readonly FunctionTransport api; + internal FunctionSecrets(FunctionTransport api) => this.api = api; + public Task>> ListAsync(string functionId, ListFunctionsParameters? parameters = null, RequestOptions? options = null) => api.Request>(HttpMethod.Get, "/" + FunctionTransport.Id(functionId) + "/secrets", parameters, options); + public Task> CreateAsync(string functionId, CreateFunctionSecretInput input, RequestOptions? options = null) => api.Request(HttpMethod.Post, "/" + FunctionTransport.Id(functionId) + "/secrets", input, options); + public Task> RevokeAsync(string functionId, string versionId, RequestOptions? options = null) => api.Request(HttpMethod.Delete, "/" + FunctionTransport.Id(functionId) + "/secrets/" + FunctionTransport.Id(versionId), null, options); +} +public sealed class FunctionInvocations +{ + private readonly FunctionTransport api; + internal FunctionInvocations(FunctionTransport api) => this.api = api; + public Task>> ListAsync(string functionId, ListFunctionsParameters? parameters = null, RequestOptions? options = null) => api.Request>(HttpMethod.Get, "/" + FunctionTransport.Id(functionId) + "/invocations", parameters, options); + public Task> RetrieveAsync(string functionId, string invocationId, RequestOptions? options = null) => api.Request(HttpMethod.Get, "/" + FunctionTransport.Id(functionId) + "/invocations/" + FunctionTransport.Id(invocationId), null, options); + public Task> CreateAsync(string functionId, CreateFunctionInvocationInput input, RequestOptions options) + { + if (options.IdempotencyKey is null || !Regex.IsMatch(options.IdempotencyKey, "^[\\x21-\\x7e]{1,128}$", RegexOptions.CultureInvariant)) throw FunctionTransport.Invalid("A valid invocation idempotency key is required."); + if (input.DeploymentId is not null) FunctionTransport.Id(input.DeploymentId); + return api.Request(HttpMethod.Post, "/" + FunctionTransport.Id(functionId) + "/invocations", input, options); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/HistoryChunks.cs b/packages/dotnet/src/Polymorfa.Sdk/HistoryChunks.cs new file mode 100644 index 00000000..1695464f --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/HistoryChunks.cs @@ -0,0 +1,111 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Polymorfa.Sdk; + +/// Exact wire fields remain available, including unknown fields and full-width unsigned integers. +public sealed record HistoryProtoField(uint Number, int WireType, byte[] Encoded, ulong? Varint, byte[]? Bytes) +{ + public override string ToString() => "HistoryProtoField(redacted)"; +} +public sealed record HistoryProtoMessage(byte[] Encoded, IReadOnlyList Fields) +{ + public override string ToString() => "HistoryProtoMessage(redacted)"; + public ulong? UInt64(uint number) => Fields.LastOrDefault(f => f.Number == number && f.WireType == 0)?.Varint; + public byte[]? Bytes(uint number) => Fields.LastOrDefault(f => f.Number == number && f.WireType == 2)?.Bytes; + public string? Text(uint number) + { + var bytes = Bytes(number); if (bytes is null) return null; + try { return new UTF8Encoding(false, true).GetString(bytes); } catch (DecoderFallbackException) { throw HistoryChunks.Invalid(); } + } + public IReadOnlyList Messages(uint number) => Fields.Where(f => f.Number == number && f.WireType == 2).Select(f => Decode(f.Bytes!)).ToArray(); + public static HistoryProtoMessage Decode(ReadOnlySpan encoded) + { + var source = encoded.ToArray(); var offset = 0; var fields = new List(); + ulong Varint() + { + ulong value = 0; + for (var shift = 0; shift < 70; shift += 7) { if (offset == source.Length) throw HistoryChunks.Invalid(); var b = source[offset++]; if (shift == 63 && b > 1) throw HistoryChunks.Invalid(); value |= (ulong)(b & 127) << shift; if ((b & 128) == 0) return value; } + throw HistoryChunks.Invalid(); + } + byte[] Take(ulong size) { if (size > (ulong)(source.Length - offset)) throw HistoryChunks.Invalid(); var value = source.AsSpan(offset, (int)size).ToArray(); offset += (int)size; return value; } + void SkipGroup(ulong number, int depth) + { + if (depth >= 64) throw HistoryChunks.Invalid(); + while (true) { var tag = Varint(); var field = tag >> 3; if (field == 0 || field >= 1 << 29) throw HistoryChunks.Invalid(); switch (tag & 7) { case 0: _ = Varint(); break; case 1: _ = Take(8); break; case 2: _ = Take(Varint()); break; case 3: SkipGroup(field, depth + 1); break; case 4: if (field != number) throw HistoryChunks.Invalid(); return; case 5: _ = Take(4); break; default: throw HistoryChunks.Invalid(); } } + } + while (offset < source.Length) + { + var start = offset; var tag = Varint(); var number = tag >> 3; var wire = (int)(tag & 7); + if (number == 0 || number >= 1 << 29) throw HistoryChunks.Invalid(); + ulong? value = null; byte[]? bytes = null; + switch (wire) { case 0: value = Varint(); break; case 1: bytes = Take(8); break; case 2: bytes = Take(Varint()); break; case 5: bytes = Take(4); break; case 3: SkipGroup(number, 0); break; default: throw HistoryChunks.Invalid(); } + fields.Add(new((uint)number, wire, source.AsSpan(start, offset - start).ToArray(), value, bytes)); + } + return new(source, fields); + } +} +public sealed record HistoryChunkConversation(string Id, string? Name, ulong? LastMessageTimestamp, ulong? UnreadCount, IReadOnlyList Messages, HistoryProtoMessage Proto) +{ + public override string ToString() => "HistoryChunkConversation(redacted)"; +} +public sealed record HistoryPushname(string? Id, string? Pushname, HistoryProtoMessage Proto) +{ + public override string ToString() => "HistoryPushname(redacted)"; +} +/// Common typed fields and the original full protobuf. Use DecodeWith for your application's complete schema. +public sealed record HistoryChunk(ulong SyncType, ulong? ChunkOrder, ulong? Progress, IReadOnlyList Conversations, IReadOnlyList StatusV3Messages, IReadOnlyList Pushnames, HistoryProtoMessage Proto) +{ + public T DecodeWith(Func, T> decoder) => decoder(Proto.Encoded); + public override string ToString() => "HistoryChunk(redacted)"; + internal static HistoryChunk FromProto(HistoryProtoMessage proto) => new(proto.UInt64(1) ?? 0, proto.UInt64(5), proto.UInt64(6), proto.Messages(2).Select(p => new HistoryChunkConversation(p.Text(1) ?? "", p.Text(13), p.UInt64(5), p.UInt64(6), p.Messages(2), p)).ToArray(), proto.Messages(3), proto.Messages(7).Select(p => new HistoryPushname(p.Text(1), p.Text(2), p)).ToArray(), proto); +} +public abstract record HistoryChunkInput; +public sealed record HistoryInlinePayload(byte[] InlinePayload) : HistoryChunkInput +{ + public override string ToString() => "HistoryInlinePayload(redacted)"; +} +public sealed record HistoryChunkDescriptor(string DirectPath, byte[] MediaKey, byte[] FileSha256, byte[] FileEncSha256, long? FileLength = null) : HistoryChunkInput +{ + public override string ToString() => "HistoryChunkDescriptor(redacted)"; +} +public sealed record HistoryChunkOptions +{ + public WhatsAppMediaOptions Media { get; init; } = new(); + public long MaxInflatedBytes { get; init; } = 512 * 1024 * 1024; +} +/// History is verified before inflation and protobuf parsing. No Polymorfa credential enters CDN requests. +public static class HistoryChunks +{ + internal static PolymorfaMediaIntegrityException Invalid() => WhatsAppMedia.Integrity("Invalid compressed history or protobuf payload.", "media_invalid_ciphertext"); + public static async Task DecodeAsync(ReadOnlyMemory compressed, long maxInflatedBytes = 512 * 1024 * 1024, CancellationToken cancellationToken = default) + { + if (maxInflatedBytes <= 0 || maxInflatedBytes > int.MaxValue) throw new PolymorfaConfigurationException("maxInflatedBytes must be a positive supported size."); + try + { + cancellationToken.ThrowIfCancellationRequested(); + if (compressed.IsEmpty) throw Invalid(); + // Own one snapshot before the first await. Neither pass reads caller memory again. + var snapshot = compressed.ToArray(); + var plaintext = await StrictHistoryCompression.DecodeAsync(snapshot, maxInflatedBytes, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); return HistoryChunk.FromProto(HistoryProtoMessage.Decode(plaintext)); + } + catch (OperationCanceledException) { throw new PolymorfaCancelledException(); } + catch (InvalidDataException) { throw Invalid(); } + catch (IOException) { throw Invalid(); } + } + public static async Task DownloadAsync(HistoryChunkInput input, HistoryChunkOptions? options = null) + { + options ??= new(); if (options.Media.CancellationToken.IsCancellationRequested) throw new PolymorfaCancelledException(); if (options.Media.MaxBytes <= 0) throw new PolymorfaConfigurationException("maxBytes must be positive."); + byte[] compressed; + if (input is HistoryInlinePayload inline) { if (inline.InlinePayload.LongLength > options.Media.MaxBytes) throw WhatsAppMedia.TooLarge(); compressed = inline.InlinePayload; } + else if (input is HistoryChunkDescriptor descriptor) + { + var media = new WhatsAppMediaDescriptor("document", WhatsAppMedia.Hash(descriptor.MediaKey, "mediaKey"), DirectPath: descriptor.DirectPath, FileSha256: WhatsAppMedia.Hash(descriptor.FileSha256, "fileSha256"), FileEncSha256: WhatsAppMedia.Hash(descriptor.FileEncSha256, "fileEncSha256"), FileLength: descriptor.FileLength); + var expanded = HKDF.DeriveKey(HashAlgorithmName.SHA256, media.MediaKey, 112, info: "WhatsApp History Keys"u8.ToArray()); var keys = new WhatsAppMediaKeys(expanded[..16], expanded[16..48], expanded[48..80]); var config = options.Media with { Verify = MediaVerifyMode.BeforeRelease }; + compressed = await WhatsAppMedia.ReadDownloadAsync(media, config, body => WhatsAppMedia.DecryptAsync(body, keys, media.FileSha256, media.FileEncSha256, config, media.FileLength), "md-msg-hist").ConfigureAwait(false); + } + else throw WhatsAppMedia.Invalid("Invalid history input."); + return await DecodeAsync(compressed, options.MaxInflatedBytes, options.Media.CancellationToken).ConfigureAwait(false); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/HttpTransport.cs b/packages/dotnet/src/Polymorfa.Sdk/HttpTransport.cs new file mode 100644 index 00000000..70ec0f01 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/HttpTransport.cs @@ -0,0 +1,237 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +public sealed record ResponseMetadata(int Status, string? RequestId, string? ApiVersion, int Attempts, IReadOnlyDictionary Headers, string? Transport, string? RoutingReason, string? OperationId); +public sealed record ApiResponse(T Data, ResponseMetadata Metadata); + +internal sealed class HttpTransport : IDisposable +{ + private readonly Credential? credential; + internal Credential Credential => credential ?? throw new PolymorfaConfigurationException("This transport has no credential."); + internal ClientOptions Options { get; } + private readonly HttpClient http; + private readonly bool ownsHttp; + internal static readonly JsonSerializerOptions Json = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new MessageContentConverter() } + }; + internal HttpTransport(Credential? credential, ClientOptions options) + { + this.credential = credential; Options = options with { }; + ValidateUrl(options.BaseUrl); + ValidateLimits(options.Timeout, options.MaxNetworkRetries); + if (string.IsNullOrWhiteSpace(options.ApiVersion) || options.ApiVersion.Contains('\r') || options.ApiVersion.Contains('\n')) throw new PolymorfaConfigurationException("Invalid apiVersion."); + ownsHttp = true; + var handler = options.TransportHandler ?? new SocketsHttpHandler { AllowAutoRedirect = false, UseCookies = false, Proxy = options.Proxy, UseProxy = options.Proxy is not null }; + ValidateHandler(handler); + http = new HttpClient(handler, disposeHandler: options.TransportHandler is null) { Timeout = Timeout.InfiniteTimeSpan }; + } + internal async Task> RequestAsync(HttpMethod method, string path, object? body, RequestOptions? requestOptions, IReadOnlyList>? query = null) + { + var options = IdempotencyHeaders.Prepare(requestOptions ?? new RequestOptions()); + var bytes = body is CampaignCapturedBody captured ? captured.Bytes : body is null ? null : JsonSerializer.SerializeToUtf8Bytes(body, body.GetType(), Json); + var previousAttempts = 0; + var maximumRetries = options.MaxNetworkRetries ?? Options.MaxNetworkRetries; + var safe = method == HttpMethod.Get || method == HttpMethod.Head || method == HttpMethod.Options || !string.IsNullOrEmpty(options.IdempotencyKey); + for (; ; ) + { + OpenedResponse opened; + try { opened = await OpenAsync(method, path, bytes, options with { MaxNetworkRetries = maximumRetries - previousAttempts }, "application/json", query).ConfigureAwait(false); } + catch (PolymorfaException error) when (error.Metadata is not null) { error.Metadata = error.Metadata with { Attempts = error.Metadata.Attempts + previousAttempts }; throw; } + using (opened) + { + var metadata = opened.Metadata with { Attempts = opened.Metadata.Attempts + previousAttempts }; + try { var data = await ReadAsync(opened.Response, options, metadata).ConfigureAwait(false); return new(data, metadata); } + catch (PolymorfaException error) when (error is PolymorfaConnectionException or PolymorfaTimeoutException && metadata.OperationId is null && safe && metadata.Attempts <= maximumRetries) + { + previousAttempts = metadata.Attempts; + await WaitAsync(Delay(null, previousAttempts), options.CancellationToken).ConfigureAwait(false); + } + } + } + } + internal async Task> RequestBinaryAsync(string path, HttpContent body, RequestOptions options, IReadOnlyList> query) + { + using var opened = await OpenAsync(HttpMethod.Post, path, null, options with { MaxNetworkRetries = 0 }, "application/json", query, binaryContent: body).ConfigureAwait(false); + return new(await ReadAsync(opened.Response, options, opened.Metadata).ConfigureAwait(false), opened.Metadata); + } + internal async Task> RequestEmptyMutationAsync(HttpMethod method, string path, RequestOptions options) + { + // SocketsHttpHandler may replay a bodyless POST on disconnect. Explicit + // empty content keeps the wire body empty and marks content submission. + using var body = new ByteArrayContent(Array.Empty()); + using var opened = await OpenAsync(method, path, null, options with { MaxNetworkRetries = 0 }, "application/json", binaryContent: body).ConfigureAwait(false); + return new(await ReadAsync(opened.Response, options, opened.Metadata).ConfigureAwait(false), opened.Metadata); + } + internal async Task> RequestMultipartAsync(string path, HttpContent body, RequestOptions options) + { + using var opened = await OpenAsync(HttpMethod.Put, path, null, options with { MaxNetworkRetries = 0 }, "application/json", binaryContent: body).ConfigureAwait(false); + return new(await ReadAsync(opened.Response, options, opened.Metadata).ConfigureAwait(false), opened.Metadata); + } + internal async Task> RequestTextAsync(string path, string accept, RequestOptions? requestOptions, IReadOnlyList>? query = null) + { + var options = requestOptions ?? new(); var previousAttempts = 0; var maximum = options.MaxNetworkRetries ?? Options.MaxNetworkRetries; + for (; ; ) + { + using var opened = await OpenAsync(HttpMethod.Get, path, null, options with { MaxNetworkRetries = maximum - previousAttempts }, accept, query).ConfigureAwait(false); + var metadata = opened.Metadata with { Attempts = opened.Metadata.Attempts + previousAttempts }; + using var linked = CancellationTokenSource.CreateLinkedTokenSource(options.CancellationToken); linked.CancelAfter(options.Timeout ?? Options.Timeout); + try { return new(await opened.Response.Content.ReadAsStringAsync(linked.Token).ConfigureAwait(false), metadata); } + catch (OperationCanceledException) { if (options.CancellationToken.IsCancellationRequested) throw new PolymorfaCancelledException(metadata); if (metadata.OperationId is not null || metadata.Attempts > maximum) throw new PolymorfaTimeoutException("Response body timed out.", metadata); } + catch (Exception error) when (error is IOException or HttpRequestException) { if (metadata.OperationId is not null || metadata.Attempts > maximum) throw new PolymorfaConnectionException("Response body interrupted.", metadata); } + previousAttempts = metadata.Attempts; await WaitAsync(Delay(null, previousAttempts), options.CancellationToken).ConfigureAwait(false); + } + } + internal async Task OpenAsync(HttpMethod method, string path, byte[]? body, RequestOptions options, string accept, IReadOnlyList>? query = null, bool allowMediaRedirect = false, HttpContent? binaryContent = null) + { + options = IdempotencyHeaders.Prepare(options); + ValidatePath(path); + var timeout = options.Timeout ?? Options.Timeout; + var retries = options.MaxNetworkRetries ?? Options.MaxNetworkRetries; + ValidateLimits(timeout, retries); + var url = ApiUrl(path); + if (query is { Count: > 0 }) url = new UriBuilder(url) { Query = string.Join("&", query.Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}")) }.Uri; + var safe = binaryContent is null && (method == HttpMethod.Get || method == HttpMethod.Head || method == HttpMethod.Options || !string.IsNullOrEmpty(options.IdempotencyKey)); + for (var attempt = 1; ; attempt++) + { + using var request = new HttpRequestMessage(method, url); + request.Headers.Accept.ParseAdd(accept); + request.Headers.UserAgent.ParseAdd($"polymorfa-dotnet/{SdkVersion.Version}"); + if (options.Headers is not null) + foreach (var (name, value) in options.Headers) + { + if (name.Equals("authorization", StringComparison.OrdinalIgnoreCase) || name.Equals("host", StringComparison.OrdinalIgnoreCase) || name.Equals("cookie", StringComparison.OrdinalIgnoreCase)) throw new PolymorfaConfigurationException("Reserved authentication header."); + request.Headers.Remove(name); + if (!request.Headers.TryAddWithoutValidation(name, value)) throw new PolymorfaConfigurationException("Invalid request header."); + } + if (credential is not null) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", credential.Value); + request.Headers.Remove("polymorfa-version"); + request.Headers.TryAddWithoutValidation("polymorfa-version", options.ApiVersion ?? Options.ApiVersion); + if (options.IdempotencyKey is not null) { request.Headers.Remove("idempotency-key"); request.Headers.TryAddWithoutValidation("idempotency-key", options.IdempotencyKey); } + if (binaryContent is not null) request.Content = binaryContent; + else if (body is not null) { request.Content = new ByteArrayContent(body); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } + var linked = CancellationTokenSource.CreateLinkedTokenSource(options.CancellationToken); + var transferred = false; + try + { + linked.CancelAfter(timeout); + HttpResponseMessage response; + try { response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, linked.Token).ConfigureAwait(false); } + catch (OperationCanceledException) + { + if (options.CancellationToken.IsCancellationRequested) throw new PolymorfaCancelledException(); + if (safe && attempt <= retries) { await WaitAsync(Delay(null, attempt), options.CancellationToken).ConfigureAwait(false); continue; } + throw new PolymorfaTimeoutException("Request timed out."); + } + catch (HttpRequestException error) + { + if (PreparedWhatsAppUpload.HasChangedSource(error)) throw PreparedWhatsAppUpload.Changed(); + if (safe && attempt <= retries) { await WaitAsync(Delay(null, attempt), options.CancellationToken).ConfigureAwait(false); continue; } + throw new PolymorfaConnectionException("Cannot reach the Polymorfa API."); + } + var metadata = Metadata(response, attempt); + if (response.IsSuccessStatusCode || allowMediaRedirect && metadata.Status is 301 or 302 or 303 or 307 or 308) + { + transferred = allowMediaRedirect; + return new(response, metadata, transferred ? linked : null); + } + if (safe && attempt <= retries && IsRetryable(metadata.Status) && Header(response, "idempotent-replayed") != "true" && metadata.OperationId is null) + { + var delay = Delay(response, attempt); response.Dispose(); + await WaitAsync(delay, options.CancellationToken).ConfigureAwait(false); continue; + } + using (response) + { + JsonElement error; + try { error = await ReadAsync(response, options, metadata).ConfigureAwait(false); } + catch (PolymorfaServerException) { error = default; } + throw PolymorfaException.FromResponse(error, metadata); + } + } + finally { if (!transferred) linked.Dispose(); } + } + } + private async Task ReadAsync(HttpResponseMessage response, RequestOptions options, ResponseMetadata? metadata = null) + { + using var linked = CancellationTokenSource.CreateLinkedTokenSource(options.CancellationToken); + linked.CancelAfter(options.Timeout ?? Options.Timeout); + try + { + var bytes = await response.Content.ReadAsByteArrayAsync(linked.Token).ConfigureAwait(false); + if (bytes.Length == 0 || response.StatusCode is System.Net.HttpStatusCode.NoContent or System.Net.HttpStatusCode.ResetContent) return default!; + return JsonSerializer.Deserialize(bytes, Json)!; + } + catch (JsonException) { throw new PolymorfaServerException("Polymorfa returned invalid JSON.", "invalid_response", metadata); } + catch (OperationCanceledException) { if (options.CancellationToken.IsCancellationRequested) throw new PolymorfaCancelledException(metadata); throw new PolymorfaTimeoutException(metadata?.OperationId is null ? "Response body timed out." : "Response body timed out. Query the operation status before retrying.", metadata); } + catch (IOException) { throw new PolymorfaConnectionException(metadata?.OperationId is null ? "Response body interrupted." : "Response body interrupted. Query the operation status before retrying.", metadata); } + catch (HttpRequestException) { throw new PolymorfaConnectionException(metadata?.OperationId is null ? "Response body interrupted." : "Response body interrupted. Query the operation status before retrying.", metadata); } + } + internal HttpClient CreateStorageClient() + { + var handler = Options.StorageTransportHandlerFactory?.Invoke() ?? new SocketsHttpHandler { AllowAutoRedirect = false, UseCookies = false, Proxy = Options.Proxy, UseProxy = Options.Proxy is not null }; + ValidateHandler(handler); + return new HttpClient(handler, disposeHandler: true) { Timeout = Timeout.InfiniteTimeSpan }; + } + private static void ValidateHandler(HttpMessageHandler handler) + { + if (handler is SocketsHttpHandler { AllowAutoRedirect: true } or HttpClientHandler { AllowAutoRedirect: true }) + throw new PolymorfaConfigurationException("Custom HTTP handlers must disable automatic redirects."); + if (handler is DelegatingHandler delegating) + { + if (delegating.InnerHandler is null) throw new PolymorfaConfigurationException("A delegating handler requires an inner transport."); + ValidateHandler(delegating.InnerHandler); + } + } + internal static string? Header(HttpResponseMessage response, string name) => response.Headers.TryGetValues(name, out var values) || response.Content.Headers.TryGetValues(name, out values) ? string.Join(",", values) : null; + private static readonly string[] SafeHeaders = ["content-disposition", "x-trace-id", "idempotent-replayed", "content-type", "x-request-id", "request-id", "polymorfa-version", "retry-after", "x-polymorfa-transport", "x-polymorfa-routing-reason", "x-polymorfa-operation-id", "polymorfa-data-region", "x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset", "polymorfa-ratelimit-reason", "polymorfa-next-cursor"]; + internal static ResponseMetadata Metadata(HttpResponseMessage response, int attempts) + { + var headers = SafeHeaders.Select(name => (name, value: Header(response, name))).Where(p => p.value is not null).ToDictionary(p => p.name, p => p.value!); + return new((int)response.StatusCode, headers.GetValueOrDefault("x-request-id") ?? headers.GetValueOrDefault("request-id"), headers.GetValueOrDefault("polymorfa-version"), attempts, new ReadOnlyDictionary(headers), headers.GetValueOrDefault("x-polymorfa-transport"), headers.GetValueOrDefault("x-polymorfa-routing-reason"), headers.GetValueOrDefault("x-polymorfa-operation-id")); + } + internal Uri ApiUrl(string path) { ValidatePath(path); return new Uri(Options.BaseUrl.AbsoluteUri.TrimEnd('/') + path); } + internal static void ValidatePath(string path) + { + var decoded = Uri.UnescapeDataString(path); + if (!path.StartsWith('/') || path.StartsWith("//", StringComparison.Ordinal) || path.Contains('\\') || path.Contains('?') || path.Contains('#') || decoded.Contains('\\') || decoded.StartsWith("//", StringComparison.Ordinal) || decoded.Split('/').Any(p => p is "." or "..")) throw new PolymorfaValidationException("Invalid API path.", "invalid_request_path"); + } + internal static void ValidateUrl(Uri url) + { + if (!url.IsAbsoluteUri || !(url.Scheme == "https" || url.Scheme == "http" && url.IsLoopback) || url.UserInfo.Length != 0 || url.Query.Length != 0 || url.Fragment.Length != 0) throw new PolymorfaConfigurationException("Invalid baseUrl."); + } + private static void ValidateLimits(TimeSpan timeout, int retries) + { + if (timeout <= TimeSpan.Zero || timeout.TotalMilliseconds > uint.MaxValue - 1 || retries < 0) throw new PolymorfaConfigurationException("Invalid timeout or maxNetworkRetries."); + } + private static bool IsRetryable(int status) => status is 408 or 409 or 429 or >= 500; + private static TimeSpan Delay(HttpResponseMessage? response, int attempt) + { + var header = response is null ? null : Header(response, "retry-after"); + if (double.TryParse(header, NumberStyles.Float, CultureInfo.InvariantCulture, out var seconds) && double.IsFinite(seconds) && seconds >= 0) return TimeSpan.FromSeconds(Math.Min(seconds, 60)); + if (DateTimeOffset.TryParse(header, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date)) return TimeSpan.FromSeconds(Math.Clamp((date - DateTimeOffset.UtcNow).TotalSeconds, 0, 60)); + return TimeSpan.FromMilliseconds(Math.Min(500 * Math.Pow(2, Math.Min(attempt - 1, 4)), 5000) * (0.5 + Random.Shared.NextDouble() * 0.5)); + } + internal static async Task WaitAsync(TimeSpan delay, CancellationToken cancellation) + { + try { await Task.Delay(delay, cancellation).ConfigureAwait(false); } + catch (OperationCanceledException) { throw new PolymorfaCancelledException(); } + } + public void Dispose() { if (ownsHttp) http.Dispose(); } +} +internal sealed class OpenedResponse(HttpResponseMessage response, ResponseMetadata metadata, CancellationTokenSource? deadline = null) : IDisposable +{ + internal HttpResponseMessage Response { get; } = response; + internal ResponseMetadata Metadata { get; } = metadata; + private CancellationTokenSource? deadline = deadline; + internal CancellationTokenSource TakeDeadline() => Interlocked.Exchange(ref deadline, null) ?? throw new InvalidOperationException("No media deadline to transfer."); + public void Dispose() { Response.Dispose(); Interlocked.Exchange(ref deadline, null)?.Dispose(); } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/IdempotencyHeaders.cs b/packages/dotnet/src/Polymorfa.Sdk/IdempotencyHeaders.cs new file mode 100644 index 00000000..5a03b525 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/IdempotencyHeaders.cs @@ -0,0 +1,32 @@ +using System.Collections.ObjectModel; +using System.Text; + +namespace Polymorfa.Sdk; + +internal static class IdempotencyHeaders +{ + internal static RequestOptions Prepare(RequestOptions options, bool generate = false) + { + var key = options.IdempotencyKey; + var headerPresent = false; + var headers = new Dictionary(); + foreach (var (name, value) in options.Headers ?? new Dictionary()) + { + if (!name.Equals("idempotency-key", StringComparison.OrdinalIgnoreCase)) headers[name] = value; + else if (options.IdempotencyKey is null) + { + if (headerPresent && key != value) throw new PolymorfaConfigurationException("Supply one unambiguous idempotency key."); + key = value; headerPresent = true; + } + } + if ((key is not null || headerPresent) && (string.IsNullOrWhiteSpace(key) || Encoding.UTF8.GetByteCount(key) > 255 || key.Any(character => character < 32 && character != '\t' || character == 127))) + throw new PolymorfaConfigurationException("The idempotency key must contain 1 to 255 bytes and valid header characters."); + // A raw header alone does not opt an unsafe request into automatic retries. + if (!generate && options.IdempotencyKey is null && headerPresent) headers["idempotency-key"] = key!; + return options with + { + Headers = options.Headers is null ? null : new ReadOnlyDictionary(headers), + IdempotencyKey = generate ? key ?? Guid.NewGuid().ToString() : options.IdempotencyKey + }; + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/Media.cs b/packages/dotnet/src/Polymorfa.Sdk/Media.cs new file mode 100644 index 00000000..2ddc6e46 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Media.cs @@ -0,0 +1,142 @@ +using System.Globalization; +using System.Net; + +namespace Polymorfa.Sdk; + +public sealed record MessagingMediaInfo(string Id, string Session, string MessageId, string MimeType, long FileLength, bool Persisted, string? S3Url = null); +public sealed class MediaDownload : IAsyncDisposable, IDisposable +{ + public Stream Body { get; } + public string? ContentType { get; } + public long? ContentLength { get; } + public string? Filename { get; } + public bool Redirected { get; } + public ResponseMetadata Metadata { get; } + private readonly HttpResponseMessage response; + private readonly IDisposable? owner; + internal MediaDownload(Stream body, HttpResponseMessage response, ResponseMetadata metadata, bool redirected, CancellationToken cancellation, CancellationTokenSource deadline, IDisposable? owner = null) + { + Body = new CancellableStream(body, cancellation, deadline, metadata); this.response = response; this.owner = owner; Metadata = metadata; Redirected = redirected; + ContentType = response.Content.Headers.ContentType?.MediaType; ContentLength = response.Content.Headers.ContentLength; + Filename = ContentDisposition.ParseFilename(response.Content.Headers.TryGetValues("Content-Disposition", out var disposition) ? disposition.FirstOrDefault() : null); + } + public async Task ReadAllAsync(CancellationToken cancellationToken = default) { using var buffer = new MemoryStream(); await Body.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false); return buffer.ToArray(); } + public void Dispose() { Body.Dispose(); response.Dispose(); owner?.Dispose(); } + public async ValueTask DisposeAsync() { await Body.DisposeAsync().ConfigureAwait(false); response.Dispose(); owner?.Dispose(); } +} +public sealed record MediaDownloadUrl(bool Streamed, Uri? Url, DateTimeOffset? ExpiresAt, string? RequestId) +{ + public override string ToString() => $"MediaDownloadUrl(streamed={Streamed}, expiresAt={ExpiresAt}, requestId={RequestId}, url=redacted)"; +} +internal sealed class CancellableStream(Stream source, CancellationToken cancellation, CancellationTokenSource deadline, ResponseMetadata metadata) : Stream +{ + private bool completed; + private int disposed; + private readonly CancellationToken deadlineToken = deadline.Token; + public override bool CanRead => source.CanRead; + public override bool CanSeek => source.CanSeek; + public override bool CanWrite => false; + public override long Length => source.Length; + public override long Position { get => source.Position; set { source.Position = value; completed = false; } } + public override void Flush() => source.Flush(); + public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult(); + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(disposed != 0, this); + if (completed) return 0; + using var linked = CancellationTokenSource.CreateLinkedTokenSource(deadlineToken, cancellationToken); + try + { + var read = await source.ReadAsync(buffer, linked.Token).ConfigureAwait(false); + if (read == 0 && buffer.Length != 0) { completed = true; deadline.CancelAfter(Timeout.InfiniteTimeSpan); } + return read; + } + catch (OperationCanceledException) + { + if (cancellation.IsCancellationRequested || cancellationToken.IsCancellationRequested) throw new PolymorfaCancelledException(metadata); + throw new PolymorfaTimeoutException("Media response body timed out.", metadata); + } + catch (IOException) { throw new PolymorfaConnectionException("Media response body was interrupted.", metadata); } + catch (HttpRequestException) { throw new PolymorfaConnectionException("Media response body was interrupted.", metadata); } + } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + public override long Seek(long offset, SeekOrigin origin) { var position = source.Seek(offset, origin); completed = false; return position; } + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + protected override void Dispose(bool disposing) { if (disposing && Interlocked.Exchange(ref disposed, 1) == 0) { deadline.Dispose(); source.Dispose(); } base.Dispose(disposing); } + public override async ValueTask DisposeAsync() { if (Interlocked.Exchange(ref disposed, 1) == 0) { deadline.Dispose(); await source.DisposeAsync().ConfigureAwait(false); } GC.SuppressFinalize(this); } +} +public sealed class MessagingMedia : Resource +{ + internal MessagingMedia(HttpTransport http) : base(http) { } + private static string Path(string id) => "/messaging/media/" + E(id); + public Task>> RetrieveAsync(string id, RequestOptions? options = null) => Get>(Path(id) + "/info", options); + public Task> PersistAsync(string id, RequestOptions? options = null) => Post(Path(id) + "/download-and-save", null, options); + public Task DownloadStreamAsync(string id, RequestOptions? options = null) => OpenDownloadAsync(Http, Path(id), options ?? new()); + public async Task> DownloadAsync(string id, RequestOptions? options = null) { await using var download = await DownloadStreamAsync(id, options).ConfigureAwait(false); return new(await download.ReadAllAsync().ConfigureAwait(false), download.Metadata); } + public async Task DownloadUrlAsync(string id, RequestOptions? options = null) + { + options ??= new(); + using var opened = await Http.OpenAsync(HttpMethod.Get, Path(id), null, options, "*/*", allowMediaRedirect: true).ConfigureAwait(false); + if (opened.Response.IsSuccessStatusCode) return new(true, null, null, opened.Metadata.RequestId); + var url = Location(opened.Response, Http.ApiUrl(Path(id)), opened.Metadata); + return new(false, url, SignedUrlExpiry(url.AbsoluteUri), opened.Metadata.RequestId); + } + internal static async Task OpenDownloadAsync(HttpTransport http, string path, RequestOptions options) + { + var opened = await http.OpenAsync(HttpMethod.Get, path, null, options, "*/*", allowMediaRedirect: true).ConfigureAwait(false); + var response = opened.Response; + HttpClient? storage = null; + CancellationTokenSource? deadline = opened.TakeDeadline(); + try + { + var metadata = opened.Metadata; var redirected = !response.IsSuccessStatusCode; + if (redirected) + { + var url = Location(response, http.ApiUrl(path), metadata); response.Dispose(); deadline.Dispose(); deadline = null; + // A separate client sends no API authorization, cookies, or application request headers. + storage = http.CreateStorageClient(); + for (var hop = 0; ; hop++) + { + deadline?.Dispose(); deadline = CancellationTokenSource.CreateLinkedTokenSource(options.CancellationToken); deadline.CancelAfter(options.Timeout ?? http.Options.Timeout); + response = await storage.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, deadline.Token).ConfigureAwait(false); + if (response.IsSuccessStatusCode) break; + if ((int)response.StatusCode is 301 or 302 or 303 or 307 or 308 && hop < 3) { url = Location(response, url, metadata); response.Dispose(); continue; } + var status = (int)response.StatusCode; response.Dispose(); throw new PolymorfaException($"Storage returned HTTP {status}.", "media_download_failed", metadata); + } + } + var body = await response.Content.ReadAsStreamAsync(deadline!.Token).ConfigureAwait(false); + return new(body, response, metadata, redirected, options.CancellationToken, deadline, storage); + } + catch (OperationCanceledException) { response.Dispose(); deadline?.Dispose(); storage?.Dispose(); if (options.CancellationToken.IsCancellationRequested) throw new PolymorfaCancelledException(opened.Metadata); throw new PolymorfaTimeoutException("Media response headers timed out.", opened.Metadata); } + catch (HttpRequestException) { response.Dispose(); deadline?.Dispose(); storage?.Dispose(); throw new PolymorfaConnectionException("Cannot reach media storage."); } + catch { response.Dispose(); deadline?.Dispose(); storage?.Dispose(); throw; } + } + private static Uri Location(HttpResponseMessage response, Uri source, ResponseMetadata metadata) + { + var location = response.Headers.Location; + if (location is null) throw new PolymorfaException("Missing media redirect location.", "invalid_redirect", metadata); + var target = location.IsAbsoluteUri ? location : new Uri(source, location); + if (target.UserInfo.Length != 0 || target.Scheme != "https" && !(target.Scheme == "http" && target.IsLoopback)) throw new PolymorfaException("Invalid media redirect location.", "invalid_redirect", metadata); + return target; + } + /// Reads a signed URL's SigV4 or epoch expiry without making a request. + public static DateTimeOffset? SignedUrlExpiry(string value) + { + if (!Uri.TryCreate(value, UriKind.Absolute, out var url)) return null; + var query = url.Query.TrimStart('?').Split('&').Select(part => part.Split('=', 2)).Where(parts => parts.Length == 2).GroupBy(parts => Uri.UnescapeDataString(parts[0]), StringComparer.Ordinal).ToDictionary(group => group.Key, group => Uri.UnescapeDataString(group.First()[1]), StringComparer.Ordinal); + if (query.TryGetValue("X-Amz-Date", out var date) && query.TryGetValue("X-Amz-Expires", out var period) && System.Text.RegularExpressions.Regex.IsMatch(period, @"\A[0-9]+\z") && DateTimeOffset.TryParseExact(date, "yyyyMMdd'T'HHmmss'Z'", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var start) && long.TryParse(period, NumberStyles.None, CultureInfo.InvariantCulture, out var seconds)) + { + try { return start.AddSeconds(seconds); } catch (ArgumentOutOfRangeException) { return null; } + } + if (query.TryGetValue("Expires", out var epoch) && System.Text.RegularExpressions.Regex.IsMatch(epoch, @"\A[0-9]{9,11}\z") && long.TryParse(epoch, NumberStyles.None, CultureInfo.InvariantCulture, out var unix)) return DateTimeOffset.FromUnixTimeSeconds(unix); + return null; + } +} +public sealed class PlatformMedia : Resource +{ + internal PlatformMedia(HttpTransport http) : base(http) { } + public Task>> RetrieveAsync(string id, RequestOptions? options = null) => Get>("/platform/media/" + E(id), options); + public Task>> DeleteAsync(string id, RequestOptions? options = null) => Delete>("/platform/media/" + E(id), options); + public Task>> CreateUploadAsync(System.Text.Json.JsonElement? body = null, RequestOptions? options = null) => Post>("/platform/media/uploads", body, options); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/MediaFiles.cs b/packages/dotnet/src/Polymorfa.Sdk/MediaFiles.cs new file mode 100644 index 00000000..096cb72c --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/MediaFiles.cs @@ -0,0 +1,65 @@ +using System.Security.Cryptography; + +namespace Polymorfa.Sdk; + +public sealed record VerifiedMediaFile(string Path, long BytesWritten) { public override string ToString() => $"VerifiedMediaFile(bytes={BytesWritten}, path=redacted)"; } +internal static class AtomicMediaFile +{ + internal static string Scratch(string destination, string suffix) => System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(destination))!, ".pmfa-" + Guid.NewGuid().ToString("N") + suffix); + internal static FileStream Create(string path) { var options = new FileStreamOptions { Mode = FileMode.CreateNew, Access = FileAccess.ReadWrite, Share = FileShare.None, Options = FileOptions.Asynchronous | FileOptions.SequentialScan }; if (!OperatingSystem.IsWindows()) options.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite; return new(path, options); } + internal static void Remove(string path) { try { File.Delete(path); } catch (IOException) { } catch (UnauthorizedAccessException) { } } + internal static async Task CopyAsync(Stream source, string destination, long maxBytes, bool overwrite, CancellationToken cancellation) + { + if (maxBytes <= 0) throw new PolymorfaConfigurationException("maxBytes must be positive."); var scratch = Scratch(destination, ".partial"); + try { long size = 0; await using (var output = Create(scratch)) { var buffer = new byte[65536]; while (true) { var count = await source.ReadAsync(buffer, cancellation).ConfigureAwait(false); if (count == 0) break; if (size + count > maxBytes) throw new PolymorfaMediaIntegrityException("Media exceeds its permitted size.", "media_too_large"); await output.WriteAsync(buffer.AsMemory(0, count), cancellation).ConfigureAwait(false); size += count; } await output.FlushAsync(cancellation).ConfigureAwait(false); } cancellation.ThrowIfCancellationRequested(); File.Move(scratch, destination, overwrite); return new(System.IO.Path.GetFullPath(destination), size); } + finally { Remove(scratch); } + } +} +public static partial class WhatsAppMedia +{ + /// Spools encrypted bytes in the destination directory, authenticates them, then decrypts and atomically commits the verified file. + public static async Task DecryptToFileAsync(Stream encrypted, WhatsAppMediaKeys keys, string destination, byte[]? fileSha256 = null, byte[]? fileEncSha256 = null, WhatsAppMediaOptions? options = null, long? fileLength = null, bool overwrite = false) + { + options ??= new(); var limit = EncryptedLimit(options.MaxBytes, fileLength); + if (keys.Iv.Length != 16 || keys.CipherKey.Length != 32 || keys.MacKey.Length != 32) throw Invalid("Invalid derived media keys."); if (fileSha256 is not null) _ = Hash(fileSha256, "fileSha256"); if (fileEncSha256 is not null) _ = Hash(fileEncSha256, "fileEncSha256"); + var sealedPath = AtomicMediaFile.Scratch(destination, ".encrypted"); var plainPath = AtomicMediaFile.Scratch(destination, ".partial"); using var deadline = CancellationTokenSource.CreateLinkedTokenSource(options.CancellationToken); deadline.CancelAfter(options.Timeout); var cancellation = deadline.Token; + try + { + long encryptedBytes = 0; long plainBytes = 0; var tail = Array.Empty(); using var encryptedHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); using var mac = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA256, keys.MacKey); mac.AppendData(keys.Iv); + await using (var spool = AtomicMediaFile.Create(sealedPath)) + { + var buffer = new byte[65536]; + while (true) + { + var count = await encrypted.ReadAsync(buffer, cancellation).ConfigureAwait(false); if (count == 0) break; encryptedBytes += count; if (encryptedBytes > limit) throw TooLarge(); encryptedHash.AppendData(buffer, 0, count); await spool.WriteAsync(buffer.AsMemory(0, count), cancellation).ConfigureAwait(false); + var combined = new byte[tail.Length + count]; tail.CopyTo(combined, 0); buffer.AsSpan(0, count).CopyTo(combined.AsSpan(tail.Length)); var signed = Math.Max(0, combined.Length - 10); if (signed > 0) mac.AppendData(combined.AsSpan(0, signed)); tail = combined[signed..]; + } + if (encryptedBytes <= 10) throw Integrity("Encrypted media is too short.", "media_too_short"); if ((encryptedBytes - 10) % 16 != 0) throw Integrity("Ciphertext must contain complete AES blocks.", "media_invalid_ciphertext"); + if (fileEncSha256 is not null && !CryptographicOperations.FixedTimeEquals(encryptedHash.GetHashAndReset(), fileEncSha256)) throw Integrity("Encrypted media hash mismatch.", "media_enc_hash_mismatch"); + if (!CryptographicOperations.FixedTimeEquals(mac.GetHashAndReset().AsSpan(0, 10), tail)) throw Integrity("Media MAC mismatch.", "media_mac_mismatch"); + await spool.FlushAsync(cancellation).ConfigureAwait(false); spool.SetLength(encryptedBytes - 10); spool.Position = 0; + using var aes = Aes.Create(); aes.Key = keys.CipherKey; aes.IV = keys.Iv; using var decipher = aes.CreateDecryptor(); await using var decrypted = new CryptoStream(spool, decipher, CryptoStreamMode.Read, leaveOpen: true); using var plainHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + await using (var output = AtomicMediaFile.Create(plainPath)) + { + while (true) { var count = await decrypted.ReadAsync(buffer, cancellation).ConfigureAwait(false); if (count == 0) break; plainBytes += count; if (plainBytes > options.MaxBytes) throw TooLarge(); plainHash.AppendData(buffer, 0, count); await output.WriteAsync(buffer.AsMemory(0, count), cancellation).ConfigureAwait(false); } + if (fileSha256 is not null && !CryptographicOperations.FixedTimeEquals(plainHash.GetHashAndReset(), fileSha256)) throw Integrity("Plaintext media hash mismatch.", "media_hash_mismatch"); await output.FlushAsync(cancellation).ConfigureAwait(false); + } + } + cancellation.ThrowIfCancellationRequested(); File.Move(plainPath, destination, overwrite); return new(System.IO.Path.GetFullPath(destination), plainBytes); + } + catch (OperationCanceledException) { if (options.CancellationToken.IsCancellationRequested) throw new PolymorfaCancelledException(); throw new PolymorfaTimeoutException("Media verification timed out."); } + catch (CryptographicException) { throw Integrity("Invalid media padding.", "media_invalid_padding"); } + finally { AtomicMediaFile.Remove(sealedPath); AtomicMediaFile.Remove(plainPath); } + } + public static async Task DownloadToFileAsync(WhatsAppMediaDescriptor descriptor, string destination, WhatsAppMediaOptions? options = null, bool overwrite = false) + { + options ??= new(); return await ReadDownloadAsync(descriptor, options, body => DecryptToFileAsync(body, DeriveKeys(descriptor.MediaKey, descriptor.MediaKind), destination, descriptor.FileSha256, descriptor.FileEncSha256, options, descriptor.FileLength, overwrite)).ConfigureAwait(false); + } +} +public static class MediaFileExtensions +{ + public static async Task DownloadToFileAsync(this MessagingMedia media, string id, string destination, long maxBytes = 256 * 1024 * 1024, bool overwrite = false, RequestOptions? options = null) + { + options ??= new(); await using var download = await media.DownloadStreamAsync(id, options).ConfigureAwait(false); if (download.ContentLength > maxBytes) throw new PolymorfaMediaIntegrityException("Media exceeds its permitted size.", "media_too_large"); return await AtomicMediaFile.CopyAsync(download.Body, destination, maxBytes, overwrite, options.CancellationToken).ConfigureAwait(false); + } +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/MessageContent.cs b/packages/dotnet/src/Polymorfa.Sdk/MessageContent.cs new file mode 100644 index 00000000..68a76688 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/MessageContent.cs @@ -0,0 +1,58 @@ +using System.Text.Json; + +namespace Polymorfa.Sdk; + +public sealed record ProductMedia(string? Url = null, string? Base64 = null, string? MimeType = null); +public sealed record ProductMessage(string BusinessOwnerId, string Id, string Title, string CurrencyCode, long PriceAmount1000, string? Description = null, long? SalePriceAmount1000 = null, string? RetailerId = null, string? Url = null, int? ImageCount = null, ProductMedia? Image = null, string? Body = null, string? Footer = null); +public sealed record ProductListSection(IReadOnlyList ProductIds, string? Title = null); +public sealed record ProductListMessage(string BusinessOwnerId, string Title, string ButtonText, IReadOnlyList Sections, string? Description = null, string? Footer = null); +public sealed record OrderMessage(string Id, int ItemCount, string Status, string SellerId, long TotalAmount1000, string TotalCurrencyCode, string? ThumbnailBase64 = null, string? Message = null, string? Title = null, string? Token = null, string? CatalogType = null); +public sealed record ListMessageRow(string Id, string Title, string? Description = null); +public sealed record ListMessageSection(IReadOnlyList Rows, string? Title = null); +public sealed record ListMessage(string Title, string ButtonText, IReadOnlyList Sections, string? Description = null, string? Footer = null); +public sealed record MessageButton(string Type, string Text, string? Url = null, string? PhoneNumber = null, string? Id = null, string? CopyCode = null, string? BusinessPhoneNumber = null, string? CatalogProductId = null) +{ + public static MessageButton OpenUrl(string text, string url) => new("url", text, Url: url); + public static MessageButton Call(string text, string phoneNumber) => new("call", text, PhoneNumber: phoneNumber); + public static MessageButton Reply(string text, string id) => new("reply", text, Id: id); + public static MessageButton Copy(string text, string code) => new("copy", text, CopyCode: code); + public static MessageButton Catalog(string text, string businessPhoneNumber, string? catalogProductId = null) => new("catalog", text, BusinessPhoneNumber: businessPhoneNumber, CatalogProductId: catalogProductId); +} +public sealed record ButtonsMessage(string Body, IReadOnlyList Buttons, string? Title = null, string? Footer = null); +public sealed record AddressMessage(string Body, string? ButtonText = null, string? Footer = null, string? Country = null); +public sealed record FlowMessage(string Body, string ButtonText, string Id, string Token, string Action, string? Footer = null, string? Screen = null, string? DataJson = null) +{ + public static FlowMessage Navigate(string body, string buttonText, string id, string token, string screen, string? dataJson = null, string? footer = null) => new(body, buttonText, id, token, "navigate", footer, screen, dataJson); + public static FlowMessage ExchangeData(string body, string buttonText, string id, string token, string? footer = null) => new(body, buttonText, id, token, "data_exchange", footer); + public override string ToString() => $"FlowMessage(id={Id}, action={Action}, token=redacted)"; +} +public sealed record CallPermissionRequestMessage(string Body); +public sealed record PaymentOrderAmount(long Value, int Offset = 100); +public sealed record PixDynamicCodePayment(string Code, string MerchantName, string Key, string KeyType); +public sealed record PaymentLink(string Uri); +public sealed record BoletoPayment(string DigitableLine); +public sealed record OrderPaymentSettings(PixDynamicCodePayment? PixDynamicCode = null, PaymentLink? PaymentLink = null, BoletoPayment? Boleto = null); +public sealed record OrderExpiration(long Timestamp, string Description); +public sealed record OrderDetailsItem(string RetailerId, string Name, PaymentOrderAmount Amount, int Quantity, PaymentOrderAmount? SaleAmount = null); +public sealed record DescribedOrderAmount(long Value, int Offset = 100, string? Description = null, string? ProgramName = null); +public sealed record OrderDetailsItemization(IReadOnlyList Items, PaymentOrderAmount Subtotal, DescribedOrderAmount Tax, string? CatalogId = null, OrderExpiration? Expiration = null, DescribedOrderAmount? Shipping = null, DescribedOrderAmount? Discount = null); +public sealed record OrderDetailsMessage(string ReferenceId, string Type, string Body, PaymentOrderAmount TotalAmount, OrderPaymentSettings PaymentSettings, string Currency = "BRL", string? Footer = null, OrderDetailsItemization? Order = null, string? HeaderImageUrl = null); +public sealed record OrderStatusUpdate(string Status, string? Description = null); +public sealed record OrderPaymentStatusUpdate(string Status, long? Timestamp = null); +public sealed record OrderStatusMessage(string ReferenceId, string Body, string? Footer = null, OrderStatusUpdate? Order = null, OrderPaymentStatusUpdate? Payment = null); +public sealed record MessageTemplateSend(string Name, string Language, IReadOnlyList? Components = null); +public sealed record EmptyMessageObject; +public sealed record PhoneNumberRequestContent(EmptyMessageObject RequestPhoneNumber) : MessageContent; +public sealed record ProductContent(ProductMessage Product) : MessageContent; +public sealed record ProductListContent(ProductListMessage ProductList) : MessageContent; +public sealed record OrderContent(OrderMessage Order) : MessageContent; +public sealed record ListContent(ListMessage List) : MessageContent; +public sealed record ButtonsContent(ButtonsMessage Buttons) : MessageContent; +public sealed record AddressContent(AddressMessage AddressMessage) : MessageContent; +public sealed record FlowContent(FlowMessage Flow) : MessageContent; +public sealed record CallPermissionRequestContent(CallPermissionRequestMessage CallPermissionRequest) : MessageContent; +public sealed record OrderDetailsContent(OrderDetailsMessage OrderDetails) : MessageContent; +public sealed record OrderStatusContent(OrderStatusMessage OrderStatus) : MessageContent; +public sealed record TemplateContent(MessageTemplateSend Template) : MessageContent; +/// A future response content object retained without losing fields. +public sealed record UnknownMessageContent(JsonElement Value) : MessageContent; diff --git a/packages/dotnet/src/Polymorfa.Sdk/MessagingClient.cs b/packages/dotnet/src/Polymorfa.Sdk/MessagingClient.cs new file mode 100644 index 00000000..3b2f11c9 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/MessagingClient.cs @@ -0,0 +1,110 @@ +using System.Text.Json; + +namespace Polymorfa.Sdk; + +public sealed class MessagingClient : IDisposable +{ + private readonly HttpTransport http; + public MessagingSessions Sessions { get; } + public Messages Messages { get; } + public QuickLinks QuickLinks { get; } + public MessagingWebhooks Webhooks { get; } + public Calls Calls { get; } + public CloudOnboarding CloudOnboarding { get; } + public Testing Testing { get; } + public ObservationPolicies ObservationPolicies { get; } + public HybridLink HybridLink { get; } + public CloudGraph CloudGraph { get; } + public MessagingCampaigns Campaigns { get; } + public Templates Templates { get; } + public CloudTemplates CloudTemplates { get; } + public Business Business { get; } + public Channels Channels { get; } + public QuickReplies QuickReplies { get; } + public MessagingBanSafe BanSafe { get; } + public Contacts Contacts { get; } + public OfficialGroups OfficialGroups { get; } + public Catalogs Catalogs { get; } + public Groups Groups { get; } + public Chats Chats { get; } + public Labels Labels { get; } + public Presence Presence { get; } + public Profile Profile { get; } + public Privacy Privacy { get; } + public ClientTokens ClientTokens { get; } + public Identities Identities { get; } + public Users Users { get; } + public MessagingMedia Media { get; } + public WhatsAppMediaResource WhatsAppMedia { get; } + public HistorySyncResource HistorySync { get; } + public MessagingClient(Credential credential, ClientOptions? options = null) + { + if (credential.Kind != CredentialKind.ClientToken) credential.RequireServer(); + http = new(credential, options ?? new()); Sessions = new(http); Messages = new(http); QuickLinks = new(http); Webhooks = new(http); Calls = new(http); CloudOnboarding = new(http); Testing = new(http); ObservationPolicies = new(http); HybridLink = new(http); Templates = new(http); Campaigns = new(http); CloudGraph = new(http); CloudTemplates = new(http); Business = new(http); Channels = new(http); QuickReplies = new(http); BanSafe = new(http); Contacts = new(http); OfficialGroups = new(http); Catalogs = new(http); Groups = new(http); Chats = new(http); Labels = new(http); Presence = new(http); Profile = new(http); Privacy = new(http); ClientTokens = new(http); Identities = new(http); Users = new(http); Media = new(http); WhatsAppMedia = new(http); HistorySync = new(http); + } + public Task> PaginateRawAsync(RawRequest request, RawPageDecoder decode, string cursorParameter = "cursor") => RawCursorPage.LoadAsync(http, request, decode, cursorParameter); + public Task> RawAsync(HttpMethod method, string path, JsonElement? body = null, RequestOptions? options = null, IReadOnlyList>? query = null) => http.RequestAsync(method, path, body, options, query); + public void Dispose() => http.Dispose(); +} +public sealed partial class MessagingSessions +{ + private readonly HttpTransport http; + internal MessagingSessions(HttpTransport http) => this.http = http; + private static string Path(string session) => $"/platform/sessions/{Uri.EscapeDataString(session)}"; + public Task>>> ListAsync(RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync>>(HttpMethod.Get, "/platform/sessions", null, options); } + public Task>> RetrieveAsync(string session, RequestOptions? options = null) { http.Credential.RequireServer(); return ExistingContractCodec.Request>(http, HttpMethod.Get, Path(session), options: options); } + public Task>> UpdateAsync(string session, UpdateSessionRequest body, RequestOptions? options = null) { http.Credential.RequireServer(); var captured = ConfigurationContract.Capture(body, body.Configuration, out var intent); return ExistingContractCodec.Request>(http, HttpMethod.Put, Path(session), captured, ExistingContractCodec.Options(options, intent)); } + public Task>> DeleteAsync(string session, RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync>(HttpMethod.Delete, Path(session), null, options); } + public Task>> StartAsync(string session, RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync>(HttpMethod.Post, Path(session) + "/start", null, options); } + public Task>> StopAsync(string session, RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync>(HttpMethod.Post, Path(session) + "/stop", null, options); } + public Task> RestartAsync(string session, RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync(HttpMethod.Post, Path(session) + "/restart", null, options); } + public Task> LogoutAsync(string session, RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync(HttpMethod.Post, Path(session) + "/logout", null, options); } + public Task>> AccountAsync(string session, RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync>(HttpMethod.Get, Path(session) + "/me", null, options); } + public Task>> QrAsync(string session, RequestOptions? options = null) => http.RequestAsync>(HttpMethod.Get, $"/messaging/{Uri.EscapeDataString(session)}/pair/qr", null, options, [new("format", "json")]); + public Task>> RequestPairingCodeAsync(string session, PairCodeRequest body, RequestOptions? options = null) => http.RequestAsync>(HttpMethod.Post, $"/messaging/{Uri.EscapeDataString(session)}/pair/code", body, options); +} +public sealed class Messages +{ + private readonly HttpTransport http; + internal Messages(HttpTransport http) => this.http = http; + private static string Path(string session, string action) => $"/messaging/{Uri.EscapeDataString(session)}/messages/{action}"; + public Task>> SendAsync(string session, SendMessageRequest body, RequestOptions? options = null) + { + if (body.Conversation.Id is null && body.Conversation.PhoneNumber is null && body.Conversation.Bsuid is null && body.Conversation.Username is null) throw new PolymorfaValidationException("A conversation identifier is required.", "invalid_parameter"); + var descriptor = body.Content switch { DescriptorImageContent c => (c.Image, WhatsAppUploadMediaType.Image), DescriptorVideoContent c => (c.Video, WhatsAppUploadMediaType.Video), DescriptorFileContent c => (c.File, WhatsAppUploadMediaType.Document), DescriptorVoiceContent c => (c.Voice, c.Voice.Ptt == true ? WhatsAppUploadMediaType.Ptt : WhatsAppUploadMediaType.Audio), StickerContent c => (c.Sticker, WhatsAppUploadMediaType.Sticker), _ => ((WhatsAppMediaSendDescriptor?)null, default(WhatsAppUploadMediaType)) }; + if (body.Content is StickerContent { Sticker: null }) throw new PolymorfaConfigurationException("A sticker descriptor is required."); + if (descriptor.Item1 is { } media) { media.Validate(descriptor.Item2); options = WhatsAppMediaResource.Version(options); } + return http.RequestAsync>(HttpMethod.Post, Path(session, "send"), body, (options ?? new()).WithIdempotency()); + } + public Task>> MarkSeenAsync(string session, SeenRequest body, RequestOptions? options = null) => http.RequestAsync>(HttpMethod.Post, Path(session, "seen"), body, options); + public Task>> SetTypingAsync(string session, TypingRequest body, RequestOptions? options = null) => http.RequestAsync>(HttpMethod.Post, Path(session, "typing"), body, options); + public Task>> ReactAsync(string session, ReactRequest body, RequestOptions? options = null) => http.RequestAsync>(HttpMethod.Post, Path(session, "react"), body, (options ?? new()).WithIdempotency()); + public Task>> StarAsync(string session, StarRequest body, RequestOptions? options = null) => http.RequestAsync>(HttpMethod.Post, Path(session, "star"), body, options); + public Task>> OperationStatusAsync(string session, string operationId, RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync>(HttpMethod.Get, $"/messaging/{Uri.EscapeDataString(session)}/operations/{Uri.EscapeDataString(operationId)}", null, options); } +} +public sealed class QuickLinks +{ + private readonly HttpTransport http; + internal QuickLinks(HttpTransport http) => this.http = http; + public Task>> CreateAsync(CreateQuickLinkRequest? body = null, RequestOptions? options = null) + { + http.Credential.RequireServer(); body ??= new(); + if (body.Purpose == "add_connection" && string.IsNullOrWhiteSpace(body.Session)) throw new PolymorfaValidationException("An add_connection link requires session.", "invalid_parameter"); + if (body.BillingControls is { } billing && (billing.Priority < 0 || billing.Priority > 1_000_000 || billing.LimitCredits < 0 || billing.LimitCredits > 1_000_000 || billing.LimitCredits is decimal limit && decimal.Round(limit, 6) != limit || body.Purpose == "add_connection" || body.Configuration?.Testing is not null)) throw new PolymorfaValidationException("Invalid billingControls.", "invalid_parameter"); + var captured = ConfigurationContract.QuickLink(body, out var intent); + return http.RequestAsync>(HttpMethod.Post, "/messaging/quicklinks", new CampaignCapturedBody(CampaignContractCodec.Encode(captured)), ExistingContractCodec.Options(options, intent)); + } + public Task>> RetrieveAsync(string id, RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync>(HttpMethod.Get, $"/messaging/quicklinks/{Uri.EscapeDataString(id)}", null, options); } + public Task> CancelAsync(string id, RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync(HttpMethod.Delete, $"/messaging/quicklinks/{Uri.EscapeDataString(id)}", null, options); } + public Task>> AvailabilityAsync(string projectId, string session, RequestOptions? options = null) { http.Credential.RequireServer(); return http.RequestAsync>(HttpMethod.Get, "/messaging/quicklinks/availability", null, options, [new("projectId", projectId), new("session", session)]); } +} +public sealed class MessagingWebhooks +{ + private readonly HttpTransport http; + internal MessagingWebhooks(HttpTransport http) => this.http = http; + public Task>>> ListAsync(RequestOptions? options = null) => http.RequestAsync>>(HttpMethod.Get, "/messaging/webhooks", null, options); + public Task>> CreateAsync(CreateWebhookRequest body, RequestOptions? options = null) => http.RequestAsync>(HttpMethod.Post, "/messaging/webhooks", body, options); + public Task>> RetrieveAsync(string id, RequestOptions? options = null) => http.RequestAsync>(HttpMethod.Get, $"/messaging/webhooks/{Uri.EscapeDataString(id)}", null, options); + public Task>> UpdateAsync(string id, UpdateWebhookRequest body, RequestOptions? options = null) => http.RequestAsync>(HttpMethod.Put, $"/messaging/webhooks/{Uri.EscapeDataString(id)}", body, options); + public Task> DeleteAsync(string id, RequestOptions? options = null) => http.RequestAsync(HttpMethod.Delete, $"/messaging/webhooks/{Uri.EscapeDataString(id)}", null, options); +} diff --git a/packages/dotnet/src/Polymorfa.Sdk/MessagingModels.cs b/packages/dotnet/src/Polymorfa.Sdk/MessagingModels.cs new file mode 100644 index 00000000..60b66117 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/MessagingModels.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Polymorfa.Sdk; + +public sealed record Contact(string Id, string Name, string PushName, string? Bsuid = null, string? PhoneNumber = null, string? BusinessName = null, string? ProfileUrl = null, string? Username = null); +public sealed record CheckContactResult(bool Exists, string? Bsuid = null, string? PhoneNumber = null, string? Id = null, string? Username = null); +public sealed record ContactBlocklist(string Hash, IReadOnlyList Contacts); +public sealed record ContactDevice(string Id, int Device, string? Bsuid = null, string? PhoneNumber = null, string? Username = null); +public sealed record ContactUserInfo(string Id, string Status, string PictureId, string VerifiedName, IReadOnlyList Devices, string? Bsuid = null, string? PhoneNumber = null, string? Username = null); +public sealed record ContactPicture(string Url); +public sealed record BusinessProfileCategory(string Id, string Name); +public sealed record BusinessProfileHours(string DayOfWeek, string Mode, string OpenTime, string CloseTime); +public sealed record BusinessProfile(string Id, string Address, string Email, string Description, IReadOnlyList Websites, string CoverPhotoId, IReadOnlyList Categories, IReadOnlyDictionary Options, string HoursTimeZone, IReadOnlyList Hours, string? Bsuid = null, string? PhoneNumber = null, string? Username = null); +public sealed record ProfileData(string Name, string Status, string? ProfilePicUrl = null, string? PhonePlatform = null, string? AccountType = null); +public sealed record SetProfileNameRequest(string Name); +public sealed record SetProfileStatusRequest(string Status); +public sealed record PictureRequest(string? Url = null, string? Base64 = null); +public sealed record PrivacySettings(string GroupAdd, string LastSeen, string Status, string Profile, string ReadReceipts, string Online, string CallAdd, string Messages, string Defense, string Stickers); +public sealed record PrivacySettingRequest(string Setting, string Value); +public sealed record DisappearingTimerRequest(long DurationSeconds); +public sealed record EditMessageRequest(string Text, string? Transport = null); +public sealed record GroupParticipant(string Id, bool IsAdmin, bool IsSuperAdmin, string? Bsuid = null, string? PhoneNumber = null, string? Username = null); +public sealed record Group(string Id, string Name, string Description, long CreatedAt, IReadOnlyList Participants, string OwnerId); +public sealed record GroupInviteInfo(string Id, string Subject, long CreatedAt, int Size, IReadOnlyList Participants, string CreatorId); +public sealed record GroupInviteCode(string Code); +public sealed record CreateGroupRequest(string Name, IReadOnlyList Participants); +public sealed record GroupFieldRequest(string Value); +public sealed record GroupParticipantsRequest(IReadOnlyList Participants); +public sealed record JoinGroupRequest(string Code); +public sealed record GroupAdminOnlyRequest(bool AdminsOnly); +public sealed record GroupMemberAddRequest(string Mode); +public sealed record GroupJoinApprovalRequest(bool Required); +public sealed record GroupCapability(string Key, string Kind, string? Unit, bool? Value, string? Source); +public sealed record GroupCapabilities(string Status, string? SyncedAt, string? CheckedAt, IReadOnlyList Capabilities); +public sealed record Label(string Id, string Name, int Color, int? OrderIndex = null, int? ChatCount = null, string? ObservedAt = null); +public sealed record LabelCollection(string Policy, string Status, IReadOnlyList