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..f271cc33 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,16 @@ 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 + - 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: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..114886c8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,17 @@ 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 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..9ab4650a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ ## Unreleased +- 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 diff --git a/README.md b/README.md index db18130b..f71883f5 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`. @@ -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 diff --git a/contracts/README.md b/contracts/README.md index 32a341e3..db02987c 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. @@ -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,18 @@ 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. 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..a492e08d 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,6 +9010,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" } }, { @@ -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" } }, { @@ -3278,7 +9408,27 @@ "fingerprint": "7a2563b0af92030b38692e9610dd10c99341d325de02d272448781dc652ce5d0", "typescript": { "status": "covered", - "method": "Client.auditLogs.list" + "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,6 +9719,26 @@ "typescript": { "status": "covered", "method": "Client.banSafe.getHealth" + }, + "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" } }, { @@ -3400,6 +9750,26 @@ "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" } }, { @@ -3553,7 +10183,27 @@ "fingerprint": "5af36c0853c01126308fb80f85fbfb5b91325a2be272ba504a73b0d31097fa4a", "typescript": { "status": "covered", - "method": "Client.callRetention.retrieve" + "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,6 +10463,26 @@ "typescript": { "status": "covered", "method": "Client.campaigns.events" + }, + "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" } }, { @@ -3664,6 +10494,26 @@ "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" } }, { @@ -3839,7 +10989,27 @@ "fingerprint": "fce2dc0e58e97c20b8288875ce2404e7c25f2a15304c6b1ec34dfe1749eed927", "typescript": { "status": "covered", - "method": "Client.project(projectId).functions.deployments.list" + "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,6 +11207,26 @@ "typescript": { "status": "covered", "method": "Client.apiKeys.list" + }, + "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" } }, { @@ -3928,6 +11238,26 @@ "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" } }, { @@ -4114,7 +11764,27 @@ "fingerprint": "17fa5f25183ee3d31600d88311646efc4cb7048b163aa5ac3c3e3eb8a4526b76", "typescript": { "status": "covered", - "method": "Client.project(projectId).operations.listTransitions" + "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,6 +11951,26 @@ "typescript": { "status": "covered", "method": "Client.project(projectId).webhookDeliveries.retrieveAttempt" + }, + "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" } }, { @@ -4192,6 +11982,26 @@ "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,6 +12672,26 @@ "typescript": { "status": "covered", "method": "Client.voice.audio.retrieve" + }, + "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" } }, { @@ -4447,6 +12703,26 @@ "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" } }, { @@ -4644,7 +13281,27 @@ "fingerprint": "4560fef3402f47053ab3c3a78983724f0c8b0c35ebd4afa1000cc9700dd4530f", "typescript": { "status": "covered", - "method": "Client.project(projectId).flows.update" + "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,6 +14622,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" } }, { @@ -5097,6 +14656,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" } }, { @@ -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,31 @@ "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"] } diff --git a/contracts/doc-snippets.json b/contracts/doc-snippets.json new file mode 100644 index 00000000..cbccf8db --- /dev/null +++ b/contracts/doc-snippets.json @@ -0,0 +1,813 @@ +{ + "schemaVersion": 1, + "sourceCommit": "8842dbef0acf6c5c7efb358aef77c7e938d25ac6", + "sourceFilesSha256": "5426b7ad908ee5accc4778201afc8e8db838d1efd0c8c6b1e1854e218d6da62e", + "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..fe8e6e69 --- /dev/null +++ b/contracts/fixtures/behavior.json @@ -0,0 +1,1200 @@ +{ + "schemaVersion": 1, + "sourceSdkCommit": "ff51567105b64bf6997e7330ba8eb502d875e7b9", + "apiVersion": "2026-09-22", + "organizationCredential": "pmfa_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "projectCredential": "pmfa_pt_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaA", + "scenarios": [ + { + "id": "sessions-list", + "request": { + "method": "GET", + "path": "/platform/sessions", + "headers": { + "authorization": "Bearer pmfa_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": true + }, + { + "id": "messaging-org-valid", + "client": "messaging", + "credentialType": "apiKey", + "credential": "pmfa_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": true + }, + { + "id": "project-valid", + "client": "platform", + "credentialType": "projectToken", + "credential": "pmfa_pt_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaA", + "projectId": "project_fixture", + "valid": true + }, + { + "id": "project-missing-id", + "client": "platform", + "credentialType": "projectToken", + "credential": "pmfa_pt_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaA", + "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_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaA", + "valid": false, + "field": "credential" + }, + { + "id": "project-wrong-principal", + "client": "platform", + "credentialType": "projectToken", + "credential": "pmfa_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "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_fixture", + "projectId": "project_fixture", + "valid": false, + "field": "credential" + }, + { + "id": "client-token-valid-messaging", + "client": "messaging", + "credentialType": "clientToken", + "credential": "pmfa_ct_fixture", + "valid": true + }, + { + "id": "invalid-base-url", + "client": "platform", + "credentialType": "organizationApiKey", + "credential": "pmfa_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "baseUrl": "https://user:password@example.invalid", + "valid": false, + "field": "baseUrl" + }, + { + "id": "negative-retries", + "client": "platform", + "credentialType": "organizationApiKey", + "credential": "pmfa_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "maxNetworkRetries": -1, + "valid": false + }, + { + "id": "zero-timeout", + "client": "platform", + "credentialType": "organizationApiKey", + "credential": "pmfa_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "timeoutMs": 0, + "valid": false + } + ] +} 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/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/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..3f9118b7 --- /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 organization 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/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/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/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/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/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..33437092 --- /dev/null +++ b/examples/go/web-host/bridge.js @@ -0,0 +1,130 @@ +import { definePolymorfaElements } from "@polymorfa/elements"; +definePolymorfaElements(); +let disposeInbox; +// 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 }); + } + 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, + }; + }, + 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 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..903f8fcd --- /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_" + strings.Repeat("a", 72)}, 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..22c963f3 --- /dev/null +++ b/examples/go/web-host/main_test.go @@ -0,0 +1,147 @@ +package main + +import ( + "crypto/sha256" + "encoding/json" + polymorfa "github.com/polymorfa/sdks/packages/go" + "io" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "strings" + "sync/atomic" + "testing" +) + +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_"+strings.Repeat("a", 72) || 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_"+strings.Repeat("a", 72) || 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_browser_fixture","expiresAt":"2030-01-01T00:00:00Z"}}`)) + })) + defer upstream.Close() + sdk, err := polymorfa.NewMessagingClient(polymorfa.Config{Credential: polymorfa.Credential{Kind: polymorfa.OrganizationAPIKey, Value: "pmfa_" + strings.Repeat("a", 72)}, 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_browser_fixture" || 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/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..ab210713 --- /dev/null +++ b/examples/php/web-host/bridge.js @@ -0,0 +1,130 @@ +import { definePolymorfaElements } from "@polymorfa/elements"; +definePolymorfaElements(); +let disposeInbox; +// 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 }); + } + 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, + }; + }, + 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 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..ba2ba707 --- /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_'.str_repeat('a', 93).'A','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_browser_fixture' && ($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_'.str_repeat('a',93).'A' || ($_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_'.str_repeat('a', 93).'A' || ($_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_'.str_repeat('a', 93).'A' || ($_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_browser_fixture','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..618711c8 --- /dev/null +++ b/examples/python/web_host/bridge.js @@ -0,0 +1,130 @@ +import { definePolymorfaElements } from "@polymorfa/elements"; +definePolymorfaElements(); +let disposeInbox; +// 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 }); + } + 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, + }; + }, + 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 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..dcbbb742 --- /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 organization API 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..1b34ba98 --- /dev/null +++ b/examples/rust/yew/frontend/bridge.js @@ -0,0 +1,125 @@ +import { definePolymorfaElements } from "@polymorfa/elements"; +definePolymorfaElements(); +let disposeInbox; +// 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 }); + } + 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, + }; + }, + 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 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..4daf9325 --- /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_browser_fixture","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(format!("pmfa_{}", "a".repeat(72))).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_browser_fixture","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(format!("pmfa_{}", "a".repeat(72))).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(format!("pmfa_pt_{}A", "a".repeat(93))).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..21f4fc80 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,48 @@ "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" }, "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/dotnet/README.md b/packages/dotnet/README.md new file mode 100644 index 00000000..09aa037f --- /dev/null +++ b/packages/dotnet/README.md @@ -0,0 +1,954 @@ +# 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 organization API 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; +organization credentials 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. + +### 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. diff --git a/packages/dotnet/coverage.json b/packages/dotnet/coverage.json new file mode 100644 index 00000000..c61f05ee --- /dev/null +++ b/packages/dotnet/coverage.json @@ -0,0 +1,4501 @@ +{ + "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 organization server API 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; organization API 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; organization API 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; organization API 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; organization API 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 organization server API key used by Client." + }, + { + "family": "platform", + "method": "GET", + "path": "/platform/templates/{templateId}", + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API 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 organization server API 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 organization server API 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; organization API 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; organization API 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." + } + ] +} 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..fe58d2b5 --- /dev/null +++ b/packages/dotnet/examples/aspnet-core/wwwroot/elements.ts @@ -0,0 +1,63 @@ +import { definePolymorfaElements, type PolymorfaInboxElement } from "@polymorfa/elements"; +import type { ConversationMessage, ConversationEvent, InboxChange, InboxConversation } from "@polymorfa/browser"; + +interface Message extends ConversationMessage { receipt: string } +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}); + } + 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};}, + 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 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..2ab9f849 --- /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_fixture" } }); +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..c8f9bfac --- /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); +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..532b7932 --- /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); +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); +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.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..96e7508a --- /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); +/// Organization API 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. Organization API 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). Organization API 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..2f48ecc7 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/CloudGraph.cs @@ -0,0 +1,25 @@ +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_lite_api_status")] string? MarketingMessagesLiteApiStatus = null, [property: JsonPropertyName("marketing_messages_onboarding_status")] string? MarketingMessagesOnboardingStatus = 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 class CloudGraph : Resource +{ + 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..5c5d2a12 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Configuration.cs @@ -0,0 +1,61 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Polymorfa.Sdk; + +public enum CredentialKind { OrganizationApiKey, ProjectToken, ClientToken } + +/// A validated principal. 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_[A-Za-z0-9_-]{72}$"); + public static Credential ProjectToken(string value) => Create(CredentialKind.ProjectToken, value, "^pmfa_pt_[A-Za-z0-9_-]{93}[AQgw]$"); + 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 || !Regex.IsMatch(value, pattern, RegexOptions.CultureInvariant)) + 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 organization API keys."); + if (kind != CredentialKind.ClientToken && OperatingSystem.IsBrowser()) + throw new PolymorfaConfigurationException("Server credentials cannot be used in browser/WASM applications."); + return new Credential(kind, value); + } + 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..3c074ce1 --- /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", "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..41f9578e --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/Errors.cs @@ -0,0 +1,58 @@ +using System.Text.Json; + +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(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) ? detail.Clone() : null; + } + 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..f0e2b898 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/HttpTransport.cs @@ -0,0 +1,224 @@ +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> 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..98169d62 --- /dev/null +++ b/packages/dotnet/src/Polymorfa.Sdk/MessagingClient.cs @@ -0,0 +1,109 @@ +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 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); 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..6ae0fea2 --- /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