diff --git a/.github/workflows/validate-sdks.yml b/.github/workflows/validate-sdks.yml index a0c794c..655fd42 100644 --- a/.github/workflows/validate-sdks.yml +++ b/.github/workflows/validate-sdks.yml @@ -67,6 +67,65 @@ jobs: working-directory: openapi-spec run: npm ci + - name: Detect SDK runtime version + id: sdk-version + working-directory: ${{ matrix.sdk_checkout_path }} + run: | + case "${{ matrix.language }}" in + ruby) echo "version=$(cat .ruby-version)" >> "$GITHUB_OUTPUT" ;; + python) echo "version=$(cat .python-version 2>/dev/null || grep -Po '(?<=requires-python.*>=)\d+\.\d+' pyproject.toml)" >> "$GITHUB_OUTPUT" ;; + go) echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> "$GITHUB_OUTPUT" ;; + dotnet) echo "version=$(grep -Po '(?<=net)\d+\.\d+' $(find . -name '*.csproj' -path '*/src/*' | head -1))" >> "$GITHUB_OUTPUT" ;; + php) echo "version=$(grep -Po '\"php\":\s*\"\^?\K[\d.]+' composer.json)" >> "$GITHUB_OUTPUT" ;; + esac + + - name: Setup Ruby + if: matrix.language == 'ruby' + uses: ruby/setup-ruby@0cb964fd540e0a24c900370abf38a33466142735 # v1.305.0 + with: + ruby-version: ${{ steps.sdk-version.outputs.version }} + bundler-cache: true + working-directory: ${{ matrix.sdk_checkout_path }} + + - name: Setup Python + if: matrix.language == 'python' + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ steps.sdk-version.outputs.version }} + + - name: Setup uv + if: matrix.language == 'python' + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + with: + enable-cache: true + cache-dependency-glob: '${{ matrix.sdk_checkout_path }}/**/uv.lock' + + - name: Setup Go + if: matrix.language == 'go' + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: ${{ steps.sdk-version.outputs.version }} + + - name: Setup .NET + if: matrix.language == 'dotnet' + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: ${{ steps.sdk-version.outputs.version }} + + - name: Setup PHP + if: matrix.language == 'php' + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + with: + php-version: ${{ steps.sdk-version.outputs.version }} + + - name: Install SDK formatter tools + working-directory: ${{ matrix.sdk_checkout_path }} + run: | + case "${{ matrix.language }}" in + python) uv tool install ruff ;; + php) composer install --prefer-dist --no-progress --no-interaction ;; + esac + - name: Extract baseline snapshot working-directory: openapi-spec run: | @@ -76,18 +135,38 @@ jobs: - name: Generate SDK working-directory: openapi-spec - run: npm run sdk:generate -- --lang ${{ matrix.language }} --output ".oagen/${{ matrix.language }}/sdk" + run: npm run sdk:generate -- --lang ${{ matrix.language }} --output "$GITHUB_WORKSPACE/${{ matrix.sdk_checkout_path }}" + + - name: Run SDK CI + working-directory: ${{ matrix.sdk_checkout_path }} + run: | + if [ -x script/ci ]; then + script/ci + elif [ -x scripts/ci ]; then + scripts/ci + else + echo "No script/ci or scripts/ci found, skipping" + fi - name: Extract candidate snapshot working-directory: openapi-spec run: | npm run sdk:compat-extract -- \ --lang ${{ matrix.language }} \ - --sdk-root .oagen/${{ matrix.language }}/sdk \ + --sdk-root "$GITHUB_WORKSPACE/${{ matrix.sdk_checkout_path }}" \ --output .oagen/${{ matrix.language }}/sdk + - name: Copy manifest for diagnostics + if: always() + working-directory: openapi-spec + run: | + mkdir -p .oagen/${{ matrix.language }}/sdk + cp "$GITHUB_WORKSPACE/${{ matrix.sdk_checkout_path }}/.oagen-manifest.json" \ + .oagen/${{ matrix.language }}/sdk/ 2>/dev/null || true + - name: Compat diff id: compat-diff + continue-on-error: true working-directory: openapi-spec run: npm run sdk:compat-diff -- --lang ${{ matrix.language }} @@ -105,6 +184,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: oagen-diagnostics-${{ matrix.language }} + include-hidden-files: true path: openapi-spec/.oagen/${{ matrix.language }}/ retention-days: 14 diff --git a/.gitignore b/.gitignore index 11b1111..69d269b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ dist/ *.local.json .oagen +compat-report.md diff --git a/.last-synced-sha b/.last-synced-sha index 1d29f7c..f72eb0e 100644 --- a/.last-synced-sha +++ b/.last-synced-sha @@ -1 +1 @@ -ab91ff74c791246890e2dcf9d297a758168ec447 +92db0495807c86fbbc4d45bd266a6c1f5bcbb59c diff --git a/package-lock.json b/package-lock.json index 04b3dd4..8c1539a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,8 @@ "name": "workos-openapi-spec", "version": "0.0.1", "dependencies": { - "@workos/oagen": "^0.9.0", - "@workos/oagen-emitters": "^0.6.0", + "@workos/oagen": "^0.11.0", + "@workos/oagen-emitters": "^0.6.3", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, @@ -518,9 +518,9 @@ } }, "node_modules/@workos/oagen": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@workos/oagen/-/oagen-0.9.0.tgz", - "integrity": "sha512-U+NGQChtG5nVr2uddELvnXs3cWqFbBFwmtMiZCA0eoiNnwABGHVT/kn8hCcXWae+Dy0wDj/6nPAbcN/aGVFx8g==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@workos/oagen/-/oagen-0.11.0.tgz", + "integrity": "sha512-ZaSQmJJmx7IPJdSyvA/jp9NWX5M6/tdMhJMamfXxUN19v86WoQ/3ZBS9zM3LjO4By3uB0eM46aGfKsEvTfHVsw==", "license": "MIT", "dependencies": { "@redocly/openapi-core": "^2.25.1", @@ -547,9 +547,9 @@ } }, "node_modules/@workos/oagen-emitters": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@workos/oagen-emitters/-/oagen-emitters-0.6.0.tgz", - "integrity": "sha512-GRNeR7hDRuvGwE+d92486RBnuQ8yerP2r0Pj4UUAEmPporTRA0rtkWw3ZgnO2/iagbbSNbGyafMkPxmCN4n+1A==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@workos/oagen-emitters/-/oagen-emitters-0.6.3.tgz", + "integrity": "sha512-IV0FmwcUBSQpqkkbUxvDuNf7yZlrrOpdDAdGCnJYgoQut49rTkJmpLtZuHhwKmp1geoyu9DrlYKyE/dQknFOHw==", "license": "MIT", "dependencies": { "@workos/oagen": "^0.9.0" @@ -558,6 +558,246 @@ "node": ">=24.10.0" } }, + "node_modules/@workos/oagen-emitters/node_modules/@workos/oagen": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@workos/oagen/-/oagen-0.9.2.tgz", + "integrity": "sha512-AfavAV3L3HeI4ZgAmPUYLOtBaZsIZ36AaVOmZwegbtKu9JEu+rZbrivThsJR849ddqvhQs9ijw51BNQuFSDAwQ==", + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^2.25.1", + "commander": "^13.1.0", + "dotenv": "^17.3.1", + "tree-sitter": "^0.21.1", + "tree-sitter-c-sharp": "^0.23.1", + "tree-sitter-elixir": "^0.3.5", + "tree-sitter-go": "^0.23.4", + "tree-sitter-kotlin": "^0.3.8", + "tree-sitter-php": "^0.23.12", + "tree-sitter-python": "^0.21.0", + "tree-sitter-ruby": "^0.21.0", + "tree-sitter-rust": "^0.21.0", + "tree-sitter-typescript": "^0.23.2", + "tsx": "^4.19.0", + "typescript": "^6.0.0" + }, + "bin": { + "oagen": "dist/cli/index.mjs" + }, + "engines": { + "node": ">=24.10.0" + } + }, + "node_modules/@workos/oagen-emitters/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + } + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-elixir": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/tree-sitter-elixir/-/tree-sitter-elixir-0.3.5.tgz", + "integrity": "sha512-xozQMvYK0aSolcQZAx2d84Xe/YMWFuRPYFlLVxO01bM2GITh5jyiIp0TqPCQa8754UzRAI7A83hZmfiYub5TZQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + } + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-elixir/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-go": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.23.4.tgz", + "integrity": "sha512-iQaHEs4yMa/hMo/ZCGqLfG61F0miinULU1fFh+GZreCRtKylFLtvn798ocCZjO2r/ungNZgAY1s1hPFyAwkc7w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.1", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-javascript": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.23.1.tgz", + "integrity": "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-kotlin": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/tree-sitter-kotlin/-/tree-sitter-kotlin-0.3.8.tgz", + "integrity": "sha512-A4obq6bjzmYrA+F0JLLoheFPcofFkctNaZSpnDd+GPn1SfVZLY4/GG4C0cYVBTOShuPBGGAOPLM1JWLZQV4m1g==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-kotlin/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-php": { + "version": "0.23.12", + "resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.12.tgz", + "integrity": "sha512-VwkBVOahhC2NYXK/Fuqq30NxuL/6c2hmbxEF4jrB7AyR5rLc7nT27mzF3qoi+pqx9Gy2AbXnGezF7h4MeM6YRA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-python": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.21.0.tgz", + "integrity": "sha512-IUKx7JcTVbByUx1iHGFS/QsIjx7pqwTMHL9bl/NGyhyyydbfNrpruo2C7W6V4KZrbkkCOlX8QVrCoGOFW5qecg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-python/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-ruby": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/tree-sitter-ruby/-/tree-sitter-ruby-0.21.0.tgz", + "integrity": "sha512-UrMpF9CZxKbZ5UFuPdXDuraaaYSMMlAiuzTpQXwNm7y0D48ibc9stWU5D6vDyJD0qf5/R+3yKTYHdHkqibmLSQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-rust": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/tree-sitter-rust/-/tree-sitter-rust-0.21.0.tgz", + "integrity": "sha512-unVr73YLn3VC4Qa/GF0Nk+Wom6UtI526p5kz9Rn2iZSqwIFedyCZ3e0fKCEmUJLIPGrTb/cIEdu3ZUNGzfZx7A==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-rust/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/@workos/oagen-emitters/node_modules/tree-sitter-typescript": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.23.2.tgz", + "integrity": "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2", + "tree-sitter-javascript": "^0.23.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, "node_modules/@workos/oagen/node_modules/commander": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", diff --git a/package.json b/package.json index 87cf272..01d5dec 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,17 @@ "sdk:compat-extract": "bash scripts/sdk-compat-extract.sh", "sdk:compat-diff": "bash scripts/sdk-compat-diff.sh", "sdk:compat-summary": "bash scripts/sdk-compat-summary.sh", + "sdk:compat-report": "node scripts/sdk-compat-pr-comment.mjs --artifacts-root .oagen --output compat-report.md --build-result success", + "sdk:languages": "node -e \"console.log(JSON.parse(require('fs').readFileSync('.github/sdk-matrix.json','utf8')).map(e=>e.language).join('\\n'))\"", + "sdk:generate-all": "bash scripts/sdk-generate-all.sh --parent-dir", "sdk:check": "npx oagen resolve --spec spec/open-api-spec.yaml --format json > /dev/null && echo 'Config loaded successfully'", "dev:link": "npm link @workos/oagen @workos/oagen-emitters", "dev:unlink": "npm install", "prepare": "husky" }, "dependencies": { - "@workos/oagen": "^0.9.0", - "@workos/oagen-emitters": "^0.6.0", + "@workos/oagen": "^0.11.0", + "@workos/oagen-emitters": "^0.6.3", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, diff --git a/scripts/sdk-compat-extract.sh b/scripts/sdk-compat-extract.sh index 04fd809..93fbb17 100755 --- a/scripts/sdk-compat-extract.sh +++ b/scripts/sdk-compat-extract.sh @@ -28,4 +28,5 @@ if [[ -z "$OUTPUT" ]]; then OUTPUT=".oagen/${LANG}" fi -exec npx oagen compat-extract --language "$LANG" --sdk-path "$SDK_ROOT" --output "$OUTPUT" --spec "$SPEC" +mkdir -p "$OUTPUT" +exec npx oagen compat-extract --lang "$LANG" --sdk-path "$SDK_ROOT" --output "$OUTPUT" --spec "$SPEC" diff --git a/scripts/sdk-compat-pr-comment.mjs b/scripts/sdk-compat-pr-comment.mjs index f9176f0..4f2e1c1 100644 --- a/scripts/sdk-compat-pr-comment.mjs +++ b/scripts/sdk-compat-pr-comment.mjs @@ -108,6 +108,7 @@ function pickSymbolMeta(change, baselineIndex, candidateIndex) { route: routeMatch?.route, operationId: routeMatch?.operationId ?? anyMatch?.operationId, kind: routeMatch?.kind ?? anyMatch?.kind, + sourceFile: candidateSymbol?.sourceFile ?? baselineSymbol?.sourceFile, }; } @@ -116,13 +117,6 @@ function highestSeverity(left, right) { return rank[right] > rank[left] ? right : left; } -function titleCaseCategory(category) { - return String(category) - .split('_') - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(' '); -} - function formatDetail(change) { if (change.message) return change.message; @@ -142,9 +136,19 @@ function escapeCell(value) { return String(value).replace(/\|/g, '\\|').replace(/\n/g, '
'); } +function formatParamList(parameters) { + if (!parameters?.length) return ''; + const params = parameters.map((p) => (p.required ? p.publicName : `${p.publicName}?`)); + if (params.length <= 5) return params.join(', '); + return params.slice(0, 4).join(', ') + ', \u2026'; +} + function formatSymbolReference(symbol) { if (!symbol?.fqName) return ''; - if (symbol.kind === 'callable' || symbol.kind === 'constructor') return `\`${symbol.fqName}(...)\``; + if (symbol.kind === 'callable' || symbol.kind === 'constructor') { + const params = formatParamList(symbol.parameters); + return `\`${symbol.fqName}(${params})\``; + } return `\`${symbol.fqName}\``; } @@ -177,7 +181,8 @@ function formatNowState(change, candidateSymbol, manifestEntry) { const lines = []; if (manifestEntry) { - lines.push(`\`${manifestEntry.service}.${manifestEntry.sdkMethod}(...)\``); + const params = formatParamList(candidateSymbol?.parameters); + lines.push(`\`${manifestEntry.service}.${manifestEntry.sdkMethod}(${params})\``); } else { const symbolRef = formatSymbolReference(candidateSymbol); if (symbolRef) { @@ -226,6 +231,239 @@ function buildLanguageData(dirPath) { }; } +/** + * Extract the local symbol name (part after the last dot) and normalize it + * for cross-language comparison. AdminEmails, admin_emails, adminEmails + * all become "adminemails". + */ +function normalizeLocalName(symbol) { + const local = symbol.includes('.') ? symbol.split('.').pop() : symbol; + return local.replace(/_/g, '').toLowerCase(); +} + +/** + * Merge a source row into a target bucket. When the same language already + * exists in the target, the before/after cells are concatenated (the two + * rows represent different symbols affected by the same spec change in + * that language, e.g. an options-type field + a model field). + */ +function foldRowInto(target, row) { + for (const [lang, entry] of Object.entries(row.perLanguage)) { + const existing = target.perLanguage[lang]; + if (existing) { + // Combine cells: deduplicate identical references + const prevSet = new Set(existing.previous.split('
')); + const nowSet = new Set(existing.now.split('
')); + for (const part of entry.previous.split('
')) { + if (part && !prevSet.has(part)) { + existing.previous += `
${part}`; + prevSet.add(part); + } + } + for (const part of entry.now.split('
')) { + if (part && !nowSet.has(part)) { + existing.now += `
${part}`; + nowSet.add(part); + } + } + } else { + target.perLanguage[lang] = { ...entry }; + } + } + target.severity = highestSeverity(target.severity, row.severity); + if (!target.routeKey && row.routeKey) target.routeKey = row.routeKey; + if (!target.operationId && row.operationId) target.operationId = row.operationId; + if (!target.service && row.service) target.service = row.service; + if (!target.detail && row.detail) target.detail = row.detail; +} + +/** + * Merge rows that represent the same spec-level change across languages. + * + * Two merge passes: + * + * 1. **Category-local merge** — rows with the same change category and + * normalized local symbol name are merged when their language sets + * don't overlap (original logic). + * + * 2. **Cross-category merge** — rows that share a routeKey AND a common + * merge hint (the affected field name) are folded together even if + * their categories differ and languages overlap. This catches cases + * like a parameter rename in PHP/Ruby being the same spec change as a + * field removal in dotnet/Go + * (e.g. AdminPortal.generateLink param admin_emails ↔ + * AdminPortalGenerateLinkOptions.AdminEmails). + */ +/** + * Pair symbol_removed + symbol_added rows that share an owner type and + * language set into a single symbol_renamed row. This collapses the common + * pattern where a spec field rename surfaces as a removal + addition in + * languages that use Options/Params types (dotnet, go) while appearing as a + * parameter rename in languages that use positional args (php, ruby). + * + * Only pairs when there is exactly one removal and one addition for a given + * (owner-type, language-set) combination to avoid false positives. + */ +function pairRemoveAddRows(rows) { + // Index remove/add rows by normalized (owner-type, language-set) key + const removeByKey = new Map(); + const addByKey = new Map(); + + for (const row of rows) { + if (row.category !== 'symbol_removed' && row.category !== 'symbol_added') continue; + const dot = row.symbol.lastIndexOf('.'); + if (dot === -1) continue; + const owner = row.symbol.substring(0, dot).replace(/_/g, '').toLowerCase(); + const langKey = Object.keys(row.perLanguage).sort().join(','); + const groupKey = `${owner}:${langKey}`; + + const map = row.category === 'symbol_removed' ? removeByKey : addByKey; + if (!map.has(groupKey)) map.set(groupKey, []); + map.get(groupKey).push(row); + } + + const absorbed = new Set(); + const renamed = []; + + for (const [groupKey, removeRows] of removeByKey) { + if (removeRows.length !== 1) continue; // ambiguous — skip + const addRows = addByKey.get(groupKey); + if (!addRows || addRows.length !== 1) continue; // ambiguous — skip + + const rr = removeRows[0]; + const ar = addRows[0]; + if (absorbed.has(rr) || absorbed.has(ar)) continue; + + absorbed.add(rr); + absorbed.add(ar); + + const merged = { + ...rr, + category: 'symbol_renamed', + severity: 'breaking', + perLanguage: {}, + mergeHints: [...new Set([...(rr.mergeHints ?? []), ...(ar.mergeHints ?? [])])], + }; + if (!merged.routeKey && ar.routeKey) merged.routeKey = ar.routeKey; + if (!merged.operationId && ar.operationId) merged.operationId = ar.operationId; + if (!merged.service && ar.service) merged.service = ar.service; + + for (const lang of Object.keys(rr.perLanguage)) { + merged.perLanguage[lang] = { + previous: rr.perLanguage[lang]?.previous || '—', + now: ar.perLanguage[lang]?.now || '—', + }; + } + + renamed.push(merged); + } + + return [...rows.filter((r) => !absorbed.has(r)), ...renamed]; +} + +function mergeRelatedRows(rows) { + // --- Pass 1: category + local name (existing logic) --- + const groups = new Map(); + for (const row of rows) { + const key = `${row.category}:${normalizeLocalName(row.symbol)}`; + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(row); + } + + const pass1 = []; + for (const group of groups.values()) { + if (group.length === 1) { + pass1.push(group[0]); + continue; + } + + // Greedy merge: try to fold each row into an existing bucket whose + // language set doesn't overlap. Rows that can't merge into any + // bucket start a new one (they represent genuinely distinct changes + // that happen to share a local name, e.g. admin_emails on two + // different models). + const buckets = []; + for (const row of group) { + const rowLangs = new Set(Object.keys(row.perLanguage)); + let target = null; + for (const bucket of buckets) { + const hasOverlap = [...rowLangs].some((lang) => bucket.perLanguage[lang]); + if (!hasOverlap) { + target = bucket; + break; + } + } + + if (target) { + foldRowInto(target, row); + } else { + buckets.push(row); + } + } + pass1.push(...buckets); + } + + // --- Pass 1.5: pair symbol_removed + symbol_added into symbol_renamed --- + const afterPairing = pairRemoveAddRows(pass1); + + // --- Pass 2: cross-category merge via routeKey + mergeHints --- + // Only attempt this when rows share a routeKey (same HTTP endpoint) + // and have a common normalized merge hint (the affected field name). + const routeGroups = new Map(); + for (const row of afterPairing) { + if (!row.routeKey) continue; + for (const hint of row.mergeHints ?? []) { + const key = `${row.routeKey}:${hint}`; + if (!routeGroups.has(key)) routeGroups.set(key, []); + routeGroups.get(key).push(row); + } + } + + const absorbed = new Set(); + for (const group of routeGroups.values()) { + if (group.length <= 1) continue; + const target = group[0]; + for (let i = 1; i < group.length; i++) { + const row = group[i]; + if (row === target || absorbed.has(row)) continue; + foldRowInto(target, row); + absorbed.add(row); + } + } + + // --- Pass 3: absorb non-routeKey rows into unique anchors --- + // A row without a routeKey (e.g. an Options-type field change) can be + // folded into a routeKey row (e.g. a service method param change) when + // they share a merge hint — they represent the same spec-level change + // surfacing differently across languages. Only absorb when there is + // exactly one candidate anchor to avoid ambiguous merges. + const hintToAnchor = new Map(); + for (const row of afterPairing) { + if (!row.routeKey || absorbed.has(row)) continue; + for (const hint of row.mergeHints ?? []) { + if (hint.length < 4) continue; // skip generic hints like "id" + if (!hintToAnchor.has(hint)) hintToAnchor.set(hint, new Set()); + hintToAnchor.get(hint).add(row); + } + } + + for (const row of afterPairing) { + if (row.routeKey || absorbed.has(row)) continue; + const candidates = new Set(); + for (const hint of row.mergeHints ?? []) { + if (hint.length < 4) continue; + const anchors = hintToAnchor.get(hint); + if (anchors) for (const a of anchors) candidates.add(a); + } + if (candidates.size === 1) { + foldRowInto([...candidates][0], row); + absorbed.add(row); + } + } + + return afterPairing.filter((row) => !absorbed.has(row)); +} + function buildRollup(languageData) { const languages = languageData.map((entry) => entry.language).sort(); const rows = new Map(); @@ -249,15 +487,45 @@ function buildRollup(languageData) { detail: formatDetail(change), routeKey, operationId: meta.operationId ?? '', + service: manifestEntry?.service ?? '', symbol: change.symbol, + mergeHints: [], perLanguage: {}, + kind: '', + signature: '', }; + // Collect normalized field names for cross-category merge. + // The local symbol name (e.g. "AdminEmails" from + // "AdminPortalGenerateLinkOptions.AdminEmails") and any affected + // parameter name (e.g. "adminEmails" from a parameter rename) + // are all normalized so they can match across languages. + const symbolHint = normalizeLocalName(change.symbol); + if (symbolHint && !row.mergeHints.includes(symbolHint)) row.mergeHints.push(symbolHint); + for (const key of ['parameter', 'name']) { + const oldVal = change.old?.[key]; + if (oldVal && oldVal !== '(removed)' && oldVal !== '(absent)') { + const hint = normalizeLocalName(oldVal); + if (hint && !row.mergeHints.includes(hint)) row.mergeHints.push(hint); + } + const newVal = change.new?.[key]; + if (newVal && newVal !== '(removed)' && newVal !== '(absent)') { + const hint = normalizeLocalName(newVal); + if (hint && !row.mergeHints.includes(hint)) row.mergeHints.push(hint); + } + } + row.severity = highestSeverity(row.severity, change.severity); if (!row.routeKey && routeKey) row.routeKey = routeKey; if (!row.operationId && meta.operationId) row.operationId = meta.operationId; + if (!row.service && manifestEntry?.service) row.service = manifestEntry.service; if (!row.detail && change.message) row.detail = change.message; + if (!row.kind && meta.kind) row.kind = meta.kind; + if (!row.signature && meta.candidateSymbol?.parameters?.length) { + row.signature = formatParamList(meta.candidateSymbol.parameters); + } + const langSourceFile = meta.sourceFile ?? ''; if (manifestEntries.length > 1) { row.perLanguage[entry.language] = { previous: formatPreviousState(change, meta.baselineSymbol), @@ -265,11 +533,13 @@ function buildRollup(languageData) { .map((item) => formatNowState(change, meta.candidateSymbol, item)) .filter(Boolean) .join('

'), + sourceFile: langSourceFile, }; } else if (manifestEntry) { row.perLanguage[entry.language] = { previous: formatPreviousState(change, meta.baselineSymbol), now: formatNowState(change, meta.candidateSymbol, manifestEntry), + sourceFile: langSourceFile, }; } else { row.perLanguage[entry.language] = { @@ -277,6 +547,7 @@ function buildRollup(languageData) { now: formatNowState(change, meta.candidateSymbol, undefined) || (routeKey ? `manifest entry missing for ${routeKey}` : 'non-operation symbol'), + sourceFile: langSourceFile, }; } @@ -284,18 +555,495 @@ function buildRollup(languageData) { } } + const mergedRows = mergeRelatedRows([...rows.values()]); + return { languages, missingLanguages, - rows: [...rows.values()].sort((left, right) => { + rows: mergedRows.sort((left, right) => { const severityOrder = { breaking: 0, 'soft-risk': 1, additive: 2 }; const severityDiff = severityOrder[left.severity] - severityOrder[right.severity]; if (severityDiff !== 0) return severityDiff; - return `${left.routeKey} ${left.symbol}`.localeCompare(`${right.routeKey} ${right.symbol}`); + return `${left.service} ${left.routeKey} ${left.symbol}`.localeCompare(`${right.service} ${right.routeKey} ${right.symbol}`); }), }; } +// --------------------------------------------------------------------------- +// Rendering helpers +// --------------------------------------------------------------------------- + +const KIND_LABELS = { + service_accessor: 'service', + callable: 'function', + constructor: 'constructor', + field: 'field', + property: 'property', + enum: 'enum', + enum_member: 'enum value', + alias: 'type', +}; + +function kindLabel(kind) { + return KIND_LABELS[kind] ?? ''; +} + +const CATEGORY_VERBS = { + symbol_removed: 'removed', + symbol_added: 'added', + symbol_renamed: 'renamed', + parameter_removed: 'param removed', + parameter_added_optional_terminal: 'optional param added', + parameter_added_non_terminal_optional: 'optional param added', + parameter_renamed: 'param renamed', + parameter_type_narrowed: 'param type changed', + parameter_requiredness_increased: 'param now required', + parameter_position_changed_order_sensitive: 'param reordered', + constructor_position_changed_order_sensitive: 'ctor param reordered', + constructor_reordered_named_friendly: 'ctor reordered (named-friendly)', + field_type_changed: 'type changed', + return_type_changed: 'return type changed', + enum_member_value_changed: 'enum value changed', +}; + +function categoryVerb(category) { + return CATEGORY_VERBS[category] ?? category.replace(/_/g, ' '); +} + +/** Pull the first backtick-wrapped reference out of a formatted cell. */ +function extractRef(formatted) { + if (!formatted || formatted === '—') return '—'; + const match = formatted.match(/`([^`]+)`/); + if (!match) return formatted.split('
')[0] || '—'; + const inner = match[1]; + if (inner === '(removed)' || inner === '(absent)') return '—'; + return `\`${inner}\``; +} + +/** One compact cell per language in the detailed table. */ +function compactCell(entry) { + if (!entry) return '—'; + const prev = extractRef(entry.previous); + const now = extractRef(entry.now); + if (prev === '—' && now === '—') return '—'; + if (prev === '—') return now; + if (now === '—') return prev; + return `${prev} → ${now}`; +} + +// --------------------------------------------------------------------------- +// Domain inference and compact rendering helpers +// --------------------------------------------------------------------------- + +/** + * Build a function that maps each row to a logical API domain. + * + * Resolution order: + * 1. row.service (set from the manifest for callable symbols) + * 2. Prefix-match the symbol root against known service names + * 3. Fall back to the symbol root itself (part before the first dot), + * consolidated so that if root A is a prefix of root B both map to A. + * e.g. DirectoryUser & DirectoryUserWithGroups → DirectoryUser; + * EventSchema & EventSchemaContext → EventSchema. + */ +function buildDomainResolver(rows) { + const services = new Set(); + for (const row of rows) { + if (row.service) services.add(row.service); + } + const sortedServices = [...services].sort((a, b) => b.length - a.length); + + // Collect every root (pre-dot component) and consolidate: when one root + // is a strict prefix of another, the longer one maps to the shorter. + const roots = new Set(); + for (const row of rows) { + const root = row.symbol.split('.')[0]; + if (root) roots.add(root); + } + + const rootToDomain = new Map(); + const sortedRoots = [...roots].sort((a, b) => a.length - b.length); + for (const root of roots) { + let best = root; + for (const shorter of sortedRoots) { + if (shorter.length >= root.length) break; + if (root.startsWith(shorter)) { + best = shorter; + break; + } + } + rootToDomain.set(root, best); + } + + return function deriveDomain(row) { + if (row.service) return row.service; + const root = row.symbol.split('.')[0]; + for (const svc of sortedServices) { + if (root.startsWith(svc)) return svc; + } + return rootToDomain.get(root) ?? (root || 'Other'); + }; +} + +const SUPER_CATEGORIES = { + symbol_removed: 'removed', + symbol_renamed: 'renamed', + symbol_added: 'added', + parameter_removed: 'params', + parameter_renamed: 'params', + parameter_type_narrowed: 'params', + parameter_requiredness_increased: 'params', + parameter_position_changed_order_sensitive: 'params', + parameter_added_optional_terminal: 'params', + parameter_added_non_terminal_optional: 'params', + constructor_position_changed_order_sensitive: 'params', + constructor_reordered_named_friendly: 'params', + field_type_changed: 'type_changed', + return_type_changed: 'type_changed', + enum_member_value_changed: 'type_changed', +}; + +function superCategory(category) { + return SUPER_CATEGORIES[category] ?? 'other'; +} + +/** + * Extract a compact description of a parameter change from a row, + * parsing the formatted per-language cells for parameter and position info. + */ +function describeParamChange(row) { + const firstEntry = Object.values(row.perLanguage)[0]; + if (!firstEntry) return categoryVerb(row.category); + + const oldParamMatch = firstEntry.previous?.match(/parameter:\s*`([^`]+)`/); + const newParamMatch = firstEntry.now?.match(/parameter:\s*`([^`]+)`/); + const param = oldParamMatch?.[1] ?? newParamMatch?.[1]; + + if (!param) return categoryVerb(row.category); + + if (row.category.includes('removed')) return `\`${param}\` removed`; + if (row.category.includes('renamed')) { + const newParam = newParamMatch?.[1]; + if (newParam && newParam !== param) return `\`${param}\` → \`${newParam}\``; + return `\`${param}\` renamed`; + } + if (row.category.includes('position') || row.category.includes('reordered')) { + const oldPos = firstEntry.previous?.match(/position:\s*`(\d+)`/)?.[1]; + const newPos = firstEntry.now?.match(/position:\s*`(\d+)`/)?.[1]; + if (oldPos && newPos) return `\`${param}\` moved ${oldPos}\u2192${newPos}`; + return `\`${param}\` reordered`; + } + if (row.category.includes('type')) { + const oldType = firstEntry.previous?.match(/type:\s*`([^`]+)`/)?.[1]; + const newType = firstEntry.now?.match(/type:\s*`([^`]+)`/)?.[1]; + if (oldType && newType) return `\`${param}\` type: \`${oldType}\` \u2192 \`${newType}\``; + return `\`${param}\` type changed`; + } + if (row.category.includes('required')) return `\`${param}\` now required`; + if (row.category.includes('added')) return `\`${param}\` added`; + + return `\`${param}\` ${categoryVerb(row.category)}`; +} + +/** Render changes as per-change blocks with before/after code samples. + * Rows that share the same symbol + category are collapsed into a single + * table so that e.g. three param removals on one method render as one block. + */ +function renderChangeBlocks(lines, rows, languages) { + // Group rows by symbol + category + const groups = []; + const groupIndex = new Map(); + for (const row of rows) { + const key = `${row.symbol}:${row.category}`; + let idx = groupIndex.get(key); + if (idx === undefined) { + idx = groups.length; + groupIndex.set(key, idx); + groups.push([]); + } + groups[idx].push(row); + } + + for (const group of groups) { + const first = group[0]; + + // Collect all active languages across the group + const activeLangs = languages.filter((lang) => group.some((r) => r.perLanguage[lang])); + if (activeLangs.length === 0) continue; + + const kl = kindLabel(first.kind); + const kindTag = kl ? ` _(${kl})_` : ''; + const desc = `\`${first.symbol}\` ${categoryVerb(first.category)}${kindTag}`; + const route = first.routeKey ? ` — \`${first.routeKey}\`` : ''; + lines.push(`**${desc}**${route}`); + lines.push(''); + lines.push('| Language | Before | After |'); + lines.push('| --- | --- | --- |'); + + for (const lang of activeLangs) { + const befores = []; + const afters = []; + const sourceFiles = new Set(); + for (const row of group) { + const entry = row.perLanguage[lang]; + if (!entry) continue; + if (entry.sourceFile) sourceFiles.add(entry.sourceFile); + if (entry.previous && entry.previous !== '—') befores.push(entry.previous); + if (entry.now && entry.now !== '—') afters.push(entry.now); + } + const before = befores.length > 0 ? befores.join('
') : '—'; + const after = afters.length > 0 ? afters.join('
') : '—'; + const fileSuffix = sourceFiles.size > 0 ? `
📄 ${[...sourceFiles].join(', ')}` : ''; + lines.push(`| ${lang}${fileSuffix} | ${escapeCell(before)} | ${escapeCell(after)} |`); + } + lines.push(''); + } +} + +/** Render breaking / soft-risk section: grouped by domain, then by change type. */ +function renderDetailedSection(lines, title, rows, languages, open, deriveDomain) { + lines.push(``); + lines.push(`

${title} (${rows.length})

`); + lines.push(''); + + if (!deriveDomain) deriveDomain = buildDomainResolver(rows); + + const byDomain = new Map(); + for (const row of rows) { + const domain = deriveDomain(row); + if (!byDomain.has(domain)) byDomain.set(domain, []); + byDomain.get(domain).push(row); + } + + for (const [domain, domainRows] of byDomain) { + lines.push(`#### ${domain}`); + lines.push(''); + + const removed = domainRows.filter((r) => superCategory(r.category) === 'removed'); + const renamed = domainRows.filter((r) => superCategory(r.category) === 'renamed'); + const params = domainRows.filter((r) => superCategory(r.category) === 'params'); + const typeChanged = domainRows.filter((r) => superCategory(r.category) === 'type_changed'); + const other = domainRows.filter((r) => { + const sc = superCategory(r.category); + return sc !== 'removed' && sc !== 'renamed' && sc !== 'params' && sc !== 'type_changed'; + }); + + renderRemovedRows(lines, removed, languages); + renderRenamedRows(lines, renamed, languages); + renderParamChangeRows(lines, params, languages); + renderTypeChangeRows(lines, typeChanged, languages); + if (other.length > 0) { + renderChangeBlocks(lines, other, languages); + } + } + + lines.push(''); + lines.push(''); +} + +/** Render additive section: grouped by domain with method signatures. */ +function renderCompactSection(lines, title, rows, languages, deriveDomain) { + lines.push('
'); + lines.push(`

${title} (${rows.length})

`); + lines.push(''); + + // Group by domain (matches the detailed section grouping) + if (!deriveDomain) deriveDomain = buildDomainResolver(rows); + const byDomain = new Map(); + for (const row of rows) { + const domain = deriveDomain(row); + if (!byDomain.has(domain)) byDomain.set(domain, []); + byDomain.get(domain).push(row); + } + + function formatAdditiveLangs(row) { + const affected = languages.filter((lang) => row.perLanguage[lang]); + if (affected.length === languages.length && affected.every((lang) => !row.perLanguage[lang]?.sourceFile)) { + return 'all'; + } + return affected + .map((lang) => { + const sf = row.perLanguage[lang]?.sourceFile; + return sf ? `${lang}
📄 ${sf}` : lang; + }) + .join('
'); + } + + function renderAdditiveGroup(groupRows) { + lines.push('| Change | Languages |'); + lines.push('| --- | --- |'); + for (const row of groupRows) { + const kl = kindLabel(row.kind); + const kindTag = kl ? ` _(${kl})_` : ''; + const sig = row.kind === 'callable' && row.signature ? `(${row.signature})` : ''; + lines.push(`| ${escapeCell(`\`${row.symbol}${sig}\` ${categoryVerb(row.category)}${kindTag}`)} | ${escapeCell(formatAdditiveLangs(row))} |`); + } + lines.push(''); + } + + for (const [domain, domainRows] of [...byDomain].sort((a, b) => a[0].localeCompare(b[0]))) { + lines.push(`#### ${domain}`); + lines.push(''); + renderAdditiveGroup(domainRows); + } + + lines.push('
'); + lines.push(''); +} + +// --------------------------------------------------------------------------- +// Compact sub-renderers (used by the improved renderDetailedSection) +// --------------------------------------------------------------------------- + +/** Render removed rows compactly: methods as a table, types/fields in a collapsible table. */ +function renderRemovedRows(lines, rows, languages) { + if (rows.length === 0) return; + + const methods = rows.filter( + (r) => r.kind === 'callable' || r.kind === 'constructor' || r.kind === 'service_accessor', + ); + const others = rows.filter((r) => !methods.includes(r)); + + if (methods.length > 0) { + lines.push(`**Removed methods** (${methods.length})`); + lines.push(''); + lines.push('| Method | Languages |'); + lines.push('| --- | --- |'); + for (const row of methods) { + const langs = Object.keys(row.perLanguage).sort().join(', '); + const sig = row.signature ? `(${row.signature})` : ''; + lines.push(`| \`${row.symbol}${sig}\` | ${langs} |`); + } + lines.push(''); + } + + if (others.length > 0) { + const kindCounts = {}; + for (const row of others) { + const k = kindLabel(row.kind) || 'symbol'; + kindCounts[k] = (kindCounts[k] || 0) + 1; + } + const kindsDesc = Object.entries(kindCounts) + .map(([k, c]) => `${c} ${k}${c !== 1 ? 's' : ''}`) + .join(', '); + + lines.push(`
`); + lines.push(`${kindsDesc} removed`); + lines.push(''); + lines.push('| Symbol | Kind | Languages |'); + lines.push('| --- | --- | --- |'); + for (const row of others) { + const kl = kindLabel(row.kind) || 'symbol'; + const langs = Object.keys(row.perLanguage).sort().join(', '); + lines.push(`| \`${row.symbol}\` | ${kl} | ${langs} |`); + } + lines.push(''); + lines.push('
'); + lines.push(''); + } +} + +/** Render renamed rows as a compact before/after table. */ +function renderRenamedRows(lines, rows, languages) { + if (rows.length === 0) return; + + lines.push(`**Renamed** (${rows.length})`); + lines.push(''); + lines.push('| Symbol | Before | After | Languages |'); + lines.push('| --- | --- | --- | --- |'); + + for (const row of rows) { + const langs = Object.keys(row.perLanguage).sort().join(', '); + const firstEntry = Object.values(row.perLanguage)[0]; + const before = extractRef(firstEntry?.previous); + const after = extractRef(firstEntry?.now); + lines.push( + `| \`${row.symbol}\` | ${escapeCell(before)} | ${escapeCell(after)} | ${langs} |`, + ); + } + lines.push(''); +} + +/** Render parameter changes grouped by parent method — one row per method. */ +function renderParamChangeRows(lines, rows, languages) { + if (rows.length === 0) return; + + const byMethod = new Map(); + for (const row of rows) { + if (!byMethod.has(row.symbol)) byMethod.set(row.symbol, []); + byMethod.get(row.symbol).push(row); + } + + lines.push(`**Parameter changes** (${rows.length})`); + lines.push(''); + lines.push('| Method | Changes | Languages |'); + lines.push('| --- | --- | --- |'); + + for (const [method, methodRows] of byMethod) { + const descriptions = methodRows.map((r) => describeParamChange(r)); + const langs = new Set(); + for (const r of methodRows) { + for (const lang of Object.keys(r.perLanguage)) langs.add(lang); + } + lines.push( + `| \`${method}\` | ${escapeCell(descriptions.join('; '))} | ${[...langs].sort().join(', ')} |`, + ); + } + lines.push(''); +} + +/** Render type/field/enum changes using the existing Before/After block format. */ +function renderTypeChangeRows(lines, rows, languages) { + if (rows.length === 0) return; + + lines.push(`**Type changes** (${rows.length})`); + lines.push(''); + renderChangeBlocks(lines, rows, languages); +} + +/** Render a domain-grouped summary table showing change counts per API domain. */ +function renderDomainSummary(lines, rows, languages, deriveDomain) { + if (!deriveDomain) deriveDomain = buildDomainResolver(rows); + const domainData = new Map(); + + for (const row of rows) { + const domain = deriveDomain(row); + if (!domainData.has(domain)) { + domainData.set(domain, { breaking: 0, softRisk: 0, additive: 0, languages: new Set() }); + } + const d = domainData.get(domain); + if (row.severity === 'breaking') d.breaking++; + else if (row.severity === 'soft-risk') d.softRisk++; + else d.additive++; + for (const lang of Object.keys(row.perLanguage)) d.languages.add(lang); + } + + const sorted = [...domainData.entries()].sort((a, b) => { + const totalA = a[1].breaking * 100 + a[1].softRisk * 10 + a[1].additive; + const totalB = b[1].breaking * 100 + b[1].softRisk * 10 + b[1].additive; + return totalB - totalA; + }); + + lines.push('### Changes by domain'); + lines.push(''); + lines.push('| Domain | Breaking | Soft-risk | Additive | Languages |'); + lines.push('| --- | --- | --- | --- | --- |'); + + for (const [domain, data] of sorted) { + const b = data.breaking || '\u2014'; + const s = data.softRisk || '\u2014'; + const a = data.additive || '\u2014'; + const langs = + data.languages.size === languages.length ? 'all' : [...data.languages].sort().join(', '); + lines.push(`| ${domain} | ${b} | ${s} | ${a} | ${langs} |`); + } + lines.push(''); +} + +// --------------------------------------------------------------------------- +// Main renderer +// --------------------------------------------------------------------------- + function renderMarkdown(languageData, buildResult) { const rollup = buildRollup(languageData); const lines = []; @@ -336,42 +1084,26 @@ function renderMarkdown(languageData, buildResult) { return lines.join('\n') + '\n'; } - const groupedRows = new Map(); - for (const row of rollup.rows) { - const entries = groupedRows.get(row.category) ?? []; - entries.push(row); - groupedRows.set(row.category, entries); - } + const breaking = rollup.rows.filter((r) => r.severity === 'breaking'); + const softRisk = rollup.rows.filter((r) => r.severity === 'soft-risk'); + const additive = rollup.rows.filter((r) => r.severity === 'additive'); - lines.push(''); + // Build domain resolver once from all rows so type-only domains (e.g. + // EventSchema) are correctly grouped even when they have no service field. + const deriveDomain = buildDomainResolver(rollup.rows); - for (const [category, rows] of groupedRows) { - lines.push(`### ${titleCaseCategory(category)}`); - lines.push(''); - - for (const row of rows) { - if (row.routeKey) { - lines.push(`#### \`${row.routeKey}\``); - } else { - lines.push(`#### \`${row.symbol}\``); - } - lines.push(''); - lines.push(row.detail || row.symbol); - lines.push(''); + lines.push(''); - if (row.operationId) { - lines.push(`operationId: \`${row.operationId}\``); - lines.push(''); - } + renderDomainSummary(lines, rollup.rows, rollup.languages, deriveDomain); - lines.push('| Language | Previous | Now |'); - lines.push('| --- | --- | --- |'); - for (const language of rollup.languages) { - const entry = row.perLanguage[language]; - lines.push(`| ${language} | ${escapeCell(entry?.previous ?? '—')} | ${escapeCell(entry?.now ?? '—')} |`); - } - lines.push(''); - } + if (breaking.length > 0) { + renderDetailedSection(lines, 'Breaking', breaking, rollup.languages, true, deriveDomain); + } + if (softRisk.length > 0) { + renderDetailedSection(lines, 'Soft-risk', softRisk, rollup.languages, false, deriveDomain); + } + if (additive.length > 0) { + renderCompactSection(lines, 'Additive', additive, rollup.languages, deriveDomain); } return lines.join('\n') + '\n'; diff --git a/scripts/sdk-generate-all.sh b/scripts/sdk-generate-all.sh new file mode 100755 index 0000000..45bda04 --- /dev/null +++ b/scripts/sdk-generate-all.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PARENT_DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --parent-dir) PARENT_DIR="$2"; shift 2 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +if [[ -z "$PARENT_DIR" ]]; then + echo "Usage: sdk-generate-all.sh --parent-dir " >&2 + echo " is the directory containing workos-{lang} SDK repos" >&2 + exit 1 +fi + +PARENT_DIR="$(cd "$PARENT_DIR" && pwd)" + +PASSED=() +FAILED=() + +LANGUAGES=$(npm --prefix "$REPO_ROOT" run sdk:languages --silent 2>/dev/null) + +for lang in $LANGUAGES; do + OUTPUT="$PARENT_DIR/workos-$lang" + echo "" + echo "========================================" + echo "Generating SDK: $lang -> $OUTPUT" + echo "========================================" + + if ! npm --prefix "$REPO_ROOT" run sdk:generate -- --lang "$lang" --output "$OUTPUT"; then + echo "[FAIL] Generation failed for: $lang" + FAILED+=("$lang (generation failed)") + continue + fi + + CI_SCRIPT="" + if [[ -f "$OUTPUT/scripts/ci" ]]; then + CI_SCRIPT="$OUTPUT/scripts/ci" + elif [[ -f "$OUTPUT/script/ci" ]]; then + CI_SCRIPT="$OUTPUT/script/ci" + fi + + if [[ -z "$CI_SCRIPT" ]]; then + echo "[WARN] No CI script found for: $lang (checked scripts/ci and script/ci)" + PASSED+=("$lang (no CI script)") + continue + fi + + echo "Running CI: $CI_SCRIPT" + if ! (cd "$OUTPUT" && bash "$CI_SCRIPT"); then + echo "[FAIL] CI failed for: $lang" + FAILED+=("$lang (CI failed)") + else + echo "[PASS] $lang" + PASSED+=("$lang") + fi +done + +echo "" +echo "========================================" +echo "Results" +echo "========================================" + +echo "" +echo "PASSED (${#PASSED[@]}):" +if [[ ${#PASSED[@]} -eq 0 ]]; then + echo " (none)" +else + for s in "${PASSED[@]}"; do + echo " ✓ $s" + done +fi + +echo "" +echo "FAILED (${#FAILED[@]}):" +if [[ ${#FAILED[@]} -eq 0 ]]; then + echo " (none)" +else + for s in "${FAILED[@]}"; do + echo " ✗ $s" + done +fi + +echo "" +if [[ ${#FAILED[@]} -gt 0 ]]; then + exit 1 +fi diff --git a/spec/open-api-spec.yaml b/spec/open-api-spec.yaml index 734fcea..13d91c5 100644 --- a/spec/open-api-spec.yaml +++ b/spec/open-api-spec.yaml @@ -1148,6 +1148,15 @@ paths: - message tags: - authorization + x-mutually-exclusive-body-groups: &ref_3 + resource_target: + optional: false + variants: + by_id: + - resource_id + by_external_id: + - resource_external_id + - resource_type_slug /authorization/organization_memberships/{organization_membership_id}/resources: get: operationId: AuthorizationController_listResourcesForMembership @@ -1237,7 +1246,8 @@ paths: description: >- The WorkOS ID of the parent resource. Provide this or both `parent_resource_external_id` and `parent_resource_type_slug`, but - not both. + not both. Mutually exclusive with `parent_resource_type_slug` and + `parent_resource_external_id`. schema: type: string example: authz_resource_01XYZ789 @@ -1246,7 +1256,9 @@ paths: in: query description: >- The slug of the parent resource type. Must be provided together with - `parent_resource_external_id`. + `parent_resource_external_id`. Required with + `parent_resource_external_id`. Mutually exclusive with + `parent_resource_id`. schema: type: string example: project @@ -1255,7 +1267,9 @@ paths: in: query description: >- The application-specific external identifier of the parent resource. - Must be provided together with `parent_resource_type_slug`. + Must be provided together with `parent_resource_type_slug`. Required + with `parent_resource_type_slug`. Mutually exclusive with + `parent_resource_id`. schema: type: string example: external_project_123 @@ -1326,6 +1340,270 @@ paths: - message tags: - authorization + x-mutually-exclusive-parameter-groups: + parent_resource: + optional: false + variants: + by_id: + - parent_resource_id + by_external_id: + - parent_resource_type_slug + - parent_resource_external_id + /authorization/organization_memberships/{organization_membership_id}/resources/{resource_id}/permissions: + get: + operationId: AuthorizationController_listEffectivePermissions + summary: List effective permissions for an organization membership on a resource + description: >- + Returns all permissions the organization membership effectively has on a + resource, including permissions inherited through roles assigned to + ancestor resources. + parameters: + - name: organization_membership_id + required: true + in: path + description: The ID of the organization membership. + schema: + type: string + example: om_01HXYZ123456789ABCDEFGHIJ + - name: resource_id + required: true + in: path + description: The ID of the authorization resource. + schema: + type: string + example: authz_resource_01HXYZ123456789ABCDEFGHIJ + - name: before + required: false + in: query + description: >- + An object ID that defines your place in the list. When the ID is not + present, you are at the end of the list. For example, if you make a + list request and receive 100 objects, ending with `"obj_123"`, your + subsequent call can include `before="obj_123"` to fetch a new batch + of objects before `"obj_123"`. + schema: + example: xxx_01HXYZ123456789ABCDEFGHIJ + type: string + - name: after + required: false + in: query + description: >- + An object ID that defines your place in the list. When the ID is not + present, you are at the end of the list. For example, if you make a + list request and receive 100 objects, ending with `"obj_123"`, your + subsequent call can include `after="obj_123"` to fetch a new batch + of objects after `"obj_123"`. + schema: + example: xxx_01HXYZ987654321KJIHGFEDCBA + type: string + - name: limit + required: false + in: query + description: >- + Upper limit on the number of objects to return, between `1` and + `100`. + schema: + minimum: 1 + maximum: 100 + default: 10 + example: 10 + type: integer + - name: order + required: false + in: query + description: >- + Order the results by the creation time. Supported values are `"asc"` + (ascending), `"desc"` (descending), and `"normal"` (descending with + reversed cursor semantics where `before` fetches older records and + `after` fetches newer records). Defaults to descending. + schema: + default: desc + example: desc + enum: + - normal + - desc + - asc + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizationPermissionList' + '403': + description: Forbidden + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '422': + description: Unprocessable Entity + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - authorization + /authorization/organization_memberships/{organization_membership_id}/resources/{resource_type_slug}/{external_id}/permissions: + get: + operationId: AuthorizationController_listEffectivePermissionsByExternalId + summary: >- + List effective permissions for an organization membership on a resource + by external ID + description: >- + Returns all permissions the organization membership effectively has on a + resource identified by its external ID, including permissions inherited + through roles assigned to ancestor resources. + parameters: + - name: organization_membership_id + required: true + in: path + description: The ID of the organization membership. + schema: + type: string + example: om_01HXYZ123456789ABCDEFGHIJ + - name: resource_type_slug + required: true + in: path + description: The slug of the resource type. + schema: + type: string + example: document + - name: external_id + required: true + in: path + description: An identifier you provide to reference the resource in your system. + schema: + type: string + example: doc-456 + - name: before + required: false + in: query + description: >- + An object ID that defines your place in the list. When the ID is not + present, you are at the end of the list. For example, if you make a + list request and receive 100 objects, ending with `"obj_123"`, your + subsequent call can include `before="obj_123"` to fetch a new batch + of objects before `"obj_123"`. + schema: + example: xxx_01HXYZ123456789ABCDEFGHIJ + type: string + - name: after + required: false + in: query + description: >- + An object ID that defines your place in the list. When the ID is not + present, you are at the end of the list. For example, if you make a + list request and receive 100 objects, ending with `"obj_123"`, your + subsequent call can include `after="obj_123"` to fetch a new batch + of objects after `"obj_123"`. + schema: + example: xxx_01HXYZ987654321KJIHGFEDCBA + type: string + - name: limit + required: false + in: query + description: >- + Upper limit on the number of objects to return, between `1` and + `100`. + schema: + minimum: 1 + maximum: 100 + default: 10 + example: 10 + type: integer + - name: order + required: false + in: query + description: >- + Order the results by the creation time. Supported values are `"asc"` + (ascending), `"desc"` (descending), and `"normal"` (descending with + reversed cursor semantics where `before` fetches older records and + `after` fetches newer records). Defaults to descending. + schema: + default: desc + example: desc + enum: + - normal + - desc + - asc + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizationPermissionList' + '403': + description: Forbidden + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '422': + description: Unprocessable Entity + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - authorization /authorization/organization_memberships/{organization_membership_id}/role_assignments: get: operationId: AuthorizationRoleAssignmentsController_listRoleAssignments @@ -1506,6 +1784,15 @@ paths: - message tags: - authorization + x-mutually-exclusive-body-groups: &ref_4 + resource_target: + optional: false + variants: + by_id: + - resource_id + by_external_id: + - resource_external_id + - resource_type_slug delete: operationId: AuthorizationRoleAssignmentsController_removeRoleByCriteria summary: Remove a role assignment @@ -1568,6 +1855,15 @@ paths: - message tags: - authorization + x-mutually-exclusive-body-groups: &ref_5 + resource_target: + optional: false + variants: + by_id: + - resource_id + by_external_id: + - resource_external_id + - resource_type_slug /authorization/organization_memberships/{organization_membership_id}/role_assignments/{role_assignment_id}: delete: operationId: AuthorizationRoleAssignmentsController_removeRoleById @@ -1622,10 +1918,8 @@ paths: /authorization/organizations/{organizationId}/roles: post: operationId: AuthorizationOrganizationRolesController_create - summary: Create a custom organization role - description: >- - Create a new custom organization role. When slug is omitted, it is - auto-generated from the role name. + summary: Create a custom role + description: Create a new custom role for this organization. parameters: - name: organizationId required: true @@ -1678,7 +1972,7 @@ paths: - OrganizationRole description: >- Whether the role is scoped to the environment or an - organization. + organization (custom role). example: OrganizationRole resource_type_slug: type: string @@ -1795,11 +2089,10 @@ paths: - authorization get: operationId: AuthorizationOrganizationRolesController_list - summary: List organization roles + summary: List custom roles description: >- Get a list of all roles that apply to an organization. This includes - both environment roles and organization-specific roles, returned in - priority order. + both environment roles and custom roles, returned in priority order. parameters: - name: organizationId required: true @@ -1846,10 +2139,10 @@ paths: /authorization/organizations/{organizationId}/roles/{slug}: get: operationId: AuthorizationOrganizationRolesController_get - summary: Get an organization role + summary: Get a custom role description: >- Retrieve a role that applies to an organization by its slug. This can - return either an environment role or an organization-specific role. + return either an environment role or a custom role. parameters: - name: organizationId required: true @@ -1903,7 +2196,7 @@ paths: - OrganizationRole description: >- Whether the role is scoped to the environment or an - organization. + organization (custom role). example: OrganizationRole resource_type_slug: type: string @@ -1969,10 +2262,10 @@ paths: - authorization patch: operationId: AuthorizationOrganizationRolesController_update - summary: Update an organization role + summary: Update a custom role description: >- - Update an existing custom organization role. Only the fields provided in - the request body will be updated. + Update an existing custom role. Only the fields provided in the request + body will be updated. parameters: - name: organizationId required: true @@ -2032,7 +2325,7 @@ paths: - OrganizationRole description: >- Whether the role is scoped to the environment or an - organization. + organization (custom role). example: OrganizationRole resource_type_slug: type: string @@ -2130,8 +2423,8 @@ paths: - authorization delete: operationId: AuthorizationOrganizationRolesController_delete - summary: Delete a custom organization role - description: Delete an existing custom organization role. + summary: Delete a custom role + description: Delete an existing custom role. parameters: - name: organizationId required: true @@ -2234,8 +2527,8 @@ paths: /authorization/organizations/{organizationId}/roles/{slug}/permissions: put: operationId: AuthorizationOrganizationRolePermissionsController_setPermissions - summary: Set permissions for a role - description: Replace all permissions on a role with the provided list. + summary: Set permissions for a custom role + description: Replace all permissions on a custom role with the provided list. parameters: - name: organizationId required: true @@ -2295,7 +2588,7 @@ paths: - OrganizationRole description: >- Whether the role is scoped to the environment or an - organization. + organization (custom role). example: OrganizationRole resource_type_slug: type: string @@ -2376,10 +2669,10 @@ paths: - authorization post: operationId: AuthorizationOrganizationRolePermissionsController_addPermission - summary: Add a permission to an organization role + summary: Add a permission to a custom role description: >- - Add a single permission to an organization role. If the permission is - already assigned to the role, this operation has no effect. + Add a single permission to a custom role. If the permission is already + assigned to the role, this operation has no effect. parameters: - name: organizationId required: true @@ -2439,7 +2732,7 @@ paths: - OrganizationRole description: >- Whether the role is scoped to the environment or an - organization. + organization (custom role). example: OrganizationRole resource_type_slug: type: string @@ -2537,8 +2830,8 @@ paths: /authorization/organizations/{organizationId}/roles/{slug}/permissions/{permissionSlug}: delete: operationId: AuthorizationOrganizationRolePermissionsController_removePermission - summary: Remove a permission from an organization role - description: Remove a single permission from an organization role by its slug. + summary: Remove a permission from a custom role + description: Remove a single permission from a custom role by its slug. parameters: - name: organizationId required: true @@ -2841,6 +3134,15 @@ paths: - message tags: - authorization + x-mutually-exclusive-body-groups: &ref_0 + parent_resource: + optional: true + variants: + by_id: + - parent_resource_id + by_external_id: + - parent_resource_external_id + - parent_resource_type_slug delete: operationId: AuthorizationResourcesByExternalIdController_deleteByExternalId summary: Delete an authorization resource by external ID @@ -3222,7 +3524,7 @@ paths: summary: Create a permission description: >- Create a new permission in your WorkOS environment. The permission can - then be assigned to environment roles and organization roles. + then be assigned to environment roles and custom roles. parameters: [] requestBody: required: true @@ -3589,24 +3891,38 @@ paths: schema: type: string example: project + - name: resource_external_id + required: false + in: query + description: Filter resources by external ID. + schema: + type: string + example: my-project-123 - name: parent_resource_id required: false in: query - description: Filter resources by parent resource ID. + description: >- + Filter resources by parent resource ID. Mutually exclusive with + `parent_resource_type_slug` and `parent_external_id`. schema: type: string example: authz_resource_01HXYZ123456789ABCDEFGHIJ - name: parent_resource_type_slug required: false in: query - description: Filter resources by parent resource type slug. + description: >- + Filter resources by parent resource type slug. Required with + `parent_external_id`. Mutually exclusive with `parent_resource_id`. schema: type: string example: workspace - name: parent_external_id required: false in: query - description: Filter resources by parent external ID. + description: >- + Filter resources by parent external ID. Required with + `parent_resource_type_slug`. Mutually exclusive with + `parent_resource_id`. schema: type: string example: ext-workspace-123 @@ -3652,6 +3968,15 @@ paths: - message tags: - authorization + x-mutually-exclusive-parameter-groups: + parent: + optional: true + variants: + by_id: + - parent_resource_id + by_external_id: + - parent_resource_type_slug + - parent_external_id post: operationId: AuthorizationResourcesController_create summary: Create an authorization resource @@ -3840,6 +4165,15 @@ paths: - message tags: - authorization + x-mutually-exclusive-body-groups: &ref_6 + parent_resource: + optional: true + variants: + by_id: + - parent_resource_id + by_external_id: + - parent_resource_external_id + - parent_resource_type_slug /authorization/resources/{resource_id}: get: operationId: AuthorizationResourcesController_findById @@ -4066,6 +4400,7 @@ paths: - message tags: - authorization + x-mutually-exclusive-body-groups: *ref_0 delete: operationId: AuthorizationResourcesController_delete summary: Delete an authorization resource @@ -4380,7 +4715,7 @@ paths: - OrganizationRole description: >- Whether the role is scoped to the environment or an - organization. + organization (custom role). example: EnvironmentRole resource_type_slug: type: string @@ -4626,7 +4961,7 @@ paths: - OrganizationRole description: >- Whether the role is scoped to the environment or an - organization. + organization (custom role). example: EnvironmentRole resource_type_slug: type: string @@ -4779,7 +5114,7 @@ paths: - OrganizationRole description: >- Whether the role is scoped to the environment or an - organization. + organization (custom role). example: EnvironmentRole resource_type_slug: type: string @@ -4935,7 +5270,7 @@ paths: - OrganizationRole description: >- Whether the role is scoped to the environment or an - organization. + organization (custom role). example: EnvironmentRole resource_type_slug: type: string @@ -8124,25 +8459,32 @@ paths: - message tags: - organizations.feature-flags - /portal/generate_link: + /organizations/{organizationId}/groups: post: - operationId: PortalSessionsController_create - summary: Generate a Portal Link - description: Generate a Portal Link scoped to an Organization. - parameters: [] + operationId: GroupsController_create + summary: Create a group + description: Create a new group within an organization. + parameters: + - name: organizationId + required: true + in: path + description: The ID of the organization. + schema: + type: string + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/GenerateLinkDto' + $ref: '#/components/schemas/CreateGroupDto' responses: '201': description: Created content: application/json: schema: - $ref: '#/components/schemas/PortalLinkResponse' + $ref: '#/components/schemas/Group' '400': description: Bad Request content: @@ -8150,11 +8492,17 @@ paths: schema: type: object properties: + code: + type: string + description: The error code identifying the type of error. + example: bad_request + const: bad_request message: type: string description: A human-readable description of the error. - example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + example: Request could not be processed. required: + - code - message '403': description: Forbidden @@ -8196,82 +8544,94 @@ paths: required: - message tags: - - admin-portal - /radar/attempts: - post: - operationId: RadarStandaloneController_assess - summary: Create an attempt - description: Assess a request for risk using the Radar engine and receive a verdict. - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - ip_address: - type: string - description: The IP address of the request to assess. - example: 49.78.240.97 - user_agent: - type: string - description: The user agent string of the request to assess. - example: Mozilla/5.0 - email: - type: string - format: email - description: The email address of the user making the request. - example: user@example.com - auth_method: - type: string - enum: - - Password - - Passkey - - Authenticator - - SMS_OTP - - Email_OTP - - Social - - SSO - - Other - description: The authentication method being used. - example: Password - action: - type: string - enum: - - login - - signup - - sign-up - - sign-in - - sign_up - - sign_in - - sign in - - sign up - description: The action being performed. - example: login - device_fingerprint: - type: string - description: An optional device fingerprint for the request. - example: fp_abc123 - bot_score: - type: string - description: An optional bot detection score for the request. - example: '0.1' - required: - - ip_address - - user_agent - - email - - auth_method - - action + - groups + x-feature-flag: user-groups-enabled + get: + operationId: GroupsController_list + summary: List groups + description: Get a paginated list of groups within an organization. + parameters: + - name: organizationId + required: true + in: path + description: The ID of the organization. + schema: + type: string + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + - name: before + required: false + in: query + description: >- + An object ID that defines your place in the list. When the ID is not + present, you are at the end of the list. For example, if you make a + list request and receive 100 objects, ending with `"obj_123"`, your + subsequent call can include `before="obj_123"` to fetch a new batch + of objects before `"obj_123"`. + schema: + example: xxx_01HXYZ123456789ABCDEFGHIJ + type: string + - name: after + required: false + in: query + description: >- + An object ID that defines your place in the list. When the ID is not + present, you are at the end of the list. For example, if you make a + list request and receive 100 objects, ending with `"obj_123"`, your + subsequent call can include `after="obj_123"` to fetch a new batch + of objects after `"obj_123"`. + schema: + example: xxx_01HXYZ987654321KJIHGFEDCBA + type: string + - name: limit + required: false + in: query + description: >- + Upper limit on the number of objects to return, between `1` and + `100`. + schema: + minimum: 1 + maximum: 100 + default: 10 + example: 10 + type: integer + - name: order + required: false + in: query + description: >- + Order the results by the creation time. Supported values are `"asc"` + (ascending), `"desc"` (descending), and `"normal"` (descending with + reversed cursor semantics where `before` fetches older records and + `after` fetches newer records). Defaults to descending. + schema: + default: desc + example: desc + enum: + - normal + - desc + - asc + type: string responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/RadarStandaloneResponse' - '400': - description: Standalone radar is not enabled. + $ref: '#/components/schemas/GroupList' + '403': + description: Forbidden + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '404': + description: Not Found content: application/json: schema: @@ -8284,47 +8644,37 @@ paths: required: - message tags: - - radar - /radar/attempts/{id}: - put: - operationId: RadarStandaloneController_updateRadarAttempt - summary: Update a Radar attempt - description: >- - You may optionally inform Radar that an authentication attempt or - challenge was successful using this endpoint. Some Radar controls depend - on tracking recent successful attempts, such as impossible travel. + - groups + x-feature-flag: user-groups-enabled + /organizations/{organizationId}/groups/{groupId}: + get: + operationId: GroupsController_get + summary: Get a group + description: Retrieve a group by its ID within an organization. parameters: - - name: id + - name: organizationId required: true in: path - description: The unique identifier of the Radar attempt to update. + description: The ID of the organization. schema: - example: radar_att_01HZBC6N1EB1ZY7KG32X type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - challenge_status: - type: string - description: Set to `"success"` to mark the challenge as completed. - example: success - const: success - attempt_status: - type: string - description: >- - Set to `"success"` to mark the authentication attempt as - successful. - example: success - const: success + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + - name: groupId + required: true + in: path + description: The ID of the group. + schema: + type: string + example: group_01HXYZ123456789ABCDEFGHIJ responses: - '204': - description: Radar attempt updated. - '400': - description: Bad Request + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + '403': + description: Forbidden content: application/json: schema: @@ -8350,69 +8700,89 @@ paths: required: - message tags: - - radar - /radar/lists/{type}/{action}: - post: - operationId: RadarStandaloneController_updateRadarList - summary: Add an entry to a Radar list - description: Add an entry to a Radar list. + - groups + x-feature-flag: user-groups-enabled + patch: + operationId: GroupsController_update + summary: Update a group + description: >- + Update an existing group. Only the fields provided in the request body + will be updated. parameters: - - name: type + - name: organizationId required: true in: path - description: The type of the Radar list (e.g. ip_address, domain, email). + description: The ID of the organization. schema: type: string - enum: - - ip_address - - domain - - email - - device - - user_agent - - device_fingerprint - - country - example: ip_address - - name: action + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + - name: groupId required: true in: path - description: >- - The list action indicating whether to add the entry to the allow or - block list. + description: The ID of the group. schema: type: string - enum: - - block - - allow - example: block + example: group_01HXYZ123456789ABCDEFGHIJ requestBody: required: true content: application/json: schema: - type: object - properties: - entry: - type: string - description: >- - The value to add to the list. Must match the format of the - list type (e.g. a valid IP address for `ip_address`, a valid - email for `email`). - example: 198.51.100.42 - required: - - entry + $ref: '#/components/schemas/UpdateGroupDto' responses: '200': - description: Entry already present in the list. + description: OK content: application/json: schema: - $ref: '#/components/schemas/RadarListEntryAlreadyPresentResponse' - '201': - description: Created - '204': - description: Entry successfully added to the list. + $ref: '#/components/schemas/Group' '400': description: Bad Request + content: + application/json: + schema: + type: object + properties: + code: + type: string + description: The error code identifying the type of error. + example: bad_request + const: bad_request + message: + type: string + description: A human-readable description of the error. + example: Request could not be processed. + required: + - code + - message + '403': + description: Forbidden + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '422': + description: Unprocessable Entity content: application/json: schema: @@ -8425,59 +8795,94 @@ paths: required: - message tags: - - radar + - groups + x-feature-flag: user-groups-enabled delete: - operationId: RadarStandaloneController_deleteRadarListEntry - summary: Remove an entry from a Radar list - description: Remove an entry from a Radar list. + operationId: GroupsController_delete + summary: Delete a group + description: Delete a group from an organization. parameters: - - name: type + - name: organizationId required: true in: path - description: The type of the Radar list (e.g. ip_address, domain, email). + description: The ID of the organization. schema: type: string - enum: - - ip_address - - domain - - email - - device - - user_agent - - device_fingerprint - - country - example: ip_address - - name: action + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + - name: groupId required: true in: path - description: >- - The list action indicating whether to remove the entry from the - allow or block list. + description: The ID of the group. schema: type: string - enum: - - block - - allow - example: block + example: group_01HXYZ123456789ABCDEFGHIJ + responses: + '204': + description: No Content + '403': + description: Forbidden + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - groups + x-feature-flag: user-groups-enabled + /organizations/{organizationId}/groups/{groupId}/organization-memberships: + post: + operationId: GroupMembershipsController_addMember + summary: Add a member to a Group + description: Add an organization membership to a group. + parameters: + - name: organizationId + required: true + in: path + description: Unique identifier of the Organization. + schema: + type: string + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + - name: groupId + required: true + in: path + description: Unique identifier of the Group. + schema: + type: string + example: group_01HXYZ123456789ABCDEFGHIJ requestBody: required: true content: application/json: schema: - type: object - properties: - entry: - type: string - description: >- - The value to remove from the list. Must match an existing - entry. - example: 198.51.100.42 - required: - - entry + $ref: '#/components/schemas/CreateGroupMembershipDto' responses: - '204': - description: Radar list updated. - '400': - description: Bad Request + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + '403': + description: Forbidden content: application/json: schema: @@ -8502,76 +8907,644 @@ paths: example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' required: - message + '422': + description: Unprocessable Entity + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message tags: - - radar - /sso/authorize: + - groups + x-feature-flag: user-groups-enabled get: - operationId: SsoController_authorize - summary: Initiate SSO - description: Initiates the single sign-on flow. - security: [] + operationId: GroupMembershipsController_listMembers + summary: List Group members + description: Get a list of organization memberships in a group. parameters: - - name: provider_scopes + - name: organizationId + required: true + in: path + description: Unique identifier of the Organization. + schema: + type: string + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + - name: groupId + required: true + in: path + description: Unique identifier of the Group. + schema: + type: string + example: group_01HXYZ123456789ABCDEFGHIJ + - name: before required: false in: query description: >- - Additional OAuth scopes to request from the identity provider. Only - applicable when using OAuth connections. - style: form - explode: false + An object ID that defines your place in the list. When the ID is not + present, you are at the end of the list. For example, if you make a + list request and receive 100 objects, ending with `"obj_123"`, your + subsequent call can include `before="obj_123"` to fetch a new batch + of objects before `"obj_123"`. schema: - type: array - items: - type: string - example: - - openid - - profile - - email - - name: provider_query_params + example: xxx_01HXYZ123456789ABCDEFGHIJ + type: string + - name: after required: false in: query description: >- - Key/value pairs of query parameters to pass to the OAuth provider. - Only applicable when using OAuth connections. - schema: - additionalProperties: - type: string - example: - hd: example.com - access_type: offline - type: object - - name: client_id - required: true - in: query + An object ID that defines your place in the list. When the ID is not + present, you are at the end of the list. For example, if you make a + list request and receive 100 objects, ending with `"obj_123"`, your + subsequent call can include `after="obj_123"` to fetch a new batch + of objects after `"obj_123"`. schema: + example: xxx_01HXYZ987654321KJIHGFEDCBA type: string - example: client_01HZBC6N1EB1ZY7KG32X - description: The unique identifier of the WorkOS environment client. - - name: domain + - name: limit required: false in: query - deprecated: true - schema: - type: string - example: example.com description: >- - Deprecated. Use `connection` or `organization` instead. Used to - initiate SSO for a connection by domain. The domain must be - associated with a connection in your WorkOS environment. - - name: provider + Upper limit on the number of objects to return, between `1` and + `100`. + schema: + minimum: 1 + maximum: 100 + default: 10 + example: 10 + type: integer + - name: order required: false in: query + description: >- + Order the results by the creation time. Supported values are `"asc"` + (ascending), `"desc"` (descending), and `"normal"` (descending with + reversed cursor semantics where `before` fetches older records and + `after` fetches newer records). Defaults to descending. schema: - type: string + default: desc + example: desc enum: - - AppleOAuth - - GitHubOAuth - - GoogleOAuth + - normal + - desc + - asc + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: >- + #/components/schemas/UserlandUserOrganizationMembershipBaseList + '403': + description: Forbidden + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - groups + x-feature-flag: user-groups-enabled + /organizations/{organizationId}/groups/{groupId}/organization-memberships/{omId}: + delete: + operationId: GroupMembershipsController_removeMember + summary: Remove a member from a Group + description: Remove an organization membership from a group. + parameters: + - name: organizationId + required: true + in: path + description: Unique identifier of the Organization. + schema: + type: string + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + - name: groupId + required: true + in: path + description: Unique identifier of the Group. + schema: + type: string + example: group_01HXYZ123456789ABCDEFGHIJ + - name: omId + required: true + in: path + description: Unique identifier of the Organization Membership. + schema: + type: string + example: om_01HXYZ123456789ABCDEFGHIJ + responses: + '204': + description: No Content + '403': + description: Forbidden + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - groups + x-feature-flag: user-groups-enabled + /portal/generate_link: + post: + operationId: PortalSessionsController_create + summary: Generate a Portal Link + description: Generate a Portal Link scoped to an Organization. + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateLinkDto' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/PortalLinkResponse' + '400': + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '403': + description: Forbidden + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '422': + description: Unprocessable Entity + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - admin-portal + /radar/attempts: + post: + operationId: RadarStandaloneController_assess + summary: Create an attempt + description: Assess a request for risk using the Radar engine and receive a verdict. + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + ip_address: + type: string + description: The IP address of the request to assess. + example: 49.78.240.97 + user_agent: + type: string + description: The user agent string of the request to assess. + example: Mozilla/5.0 + email: + type: string + format: email + description: The email address of the user making the request. + example: user@example.com + auth_method: + type: string + enum: + - Password + - Passkey + - Authenticator + - SMS_OTP + - Email_OTP + - Social + - SSO + - Other + description: The authentication method being used. + example: Password + action: + type: string + enum: + - login + - signup + - sign-up + - sign-in + - sign_up + - sign_in + - sign in + - sign up + description: The action being performed. + example: login + device_fingerprint: + type: string + description: An optional device fingerprint for the request. + example: fp_abc123 + bot_score: + type: string + description: An optional bot detection score for the request. + example: '0.1' + required: + - ip_address + - user_agent + - email + - auth_method + - action + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RadarStandaloneResponse' + '400': + description: Standalone radar is not enabled. + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - radar + /radar/attempts/{id}: + put: + operationId: RadarStandaloneController_updateRadarAttempt + summary: Update a Radar attempt + description: >- + You may optionally inform Radar that an authentication attempt or + challenge was successful using this endpoint. Some Radar controls depend + on tracking recent successful attempts, such as impossible travel. + parameters: + - name: id + required: true + in: path + description: The unique identifier of the Radar attempt to update. + schema: + example: radar_att_01HZBC6N1EB1ZY7KG32X + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + challenge_status: + type: string + description: Set to `"success"` to mark the challenge as completed. + example: success + const: success + attempt_status: + type: string + description: >- + Set to `"success"` to mark the authentication attempt as + successful. + example: success + const: success + responses: + '204': + description: Radar attempt updated. + '400': + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - radar + /radar/lists/{type}/{action}: + post: + operationId: RadarStandaloneController_updateRadarList + summary: Add an entry to a Radar list + description: Add an entry to a Radar list. + parameters: + - name: type + required: true + in: path + description: The type of the Radar list (e.g. ip_address, domain, email). + schema: + type: string + enum: + - ip_address + - domain + - email + - device + - user_agent + - device_fingerprint + - country + example: ip_address + - name: action + required: true + in: path + description: >- + The list action indicating whether to add the entry to the allow or + block list. + schema: + type: string + enum: + - block + - allow + example: block + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + entry: + type: string + description: >- + The value to add to the list. Must match the format of the + list type (e.g. a valid IP address for `ip_address`, a valid + email for `email`). + example: 198.51.100.42 + required: + - entry + responses: + '200': + description: Entry already present in the list. + content: + application/json: + schema: + $ref: '#/components/schemas/RadarListEntryAlreadyPresentResponse' + '201': + description: Created + '204': + description: Entry successfully added to the list. + '400': + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - radar + delete: + operationId: RadarStandaloneController_deleteRadarListEntry + summary: Remove an entry from a Radar list + description: Remove an entry from a Radar list. + parameters: + - name: type + required: true + in: path + description: The type of the Radar list (e.g. ip_address, domain, email). + schema: + type: string + enum: + - ip_address + - domain + - email + - device + - user_agent + - device_fingerprint + - country + example: ip_address + - name: action + required: true + in: path + description: >- + The list action indicating whether to remove the entry from the + allow or block list. + schema: + type: string + enum: + - block + - allow + example: block + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + entry: + type: string + description: >- + The value to remove from the list. Must match an existing + entry. + example: 198.51.100.42 + required: + - entry + responses: + '204': + description: Radar list updated. + '400': + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - radar + /sso/authorize: + get: + operationId: SsoController_authorize + summary: Initiate SSO + description: Initiates the single sign-on flow. + security: [] + parameters: + - name: provider_scopes + required: false + in: query + description: >- + Additional scopes to request from the identity provider. Applicable + when using OAuth or OpenID Connect connections. + style: form + explode: false + schema: + type: array + items: + type: string + example: + - openid + - profile + - email + - name: provider_query_params + required: false + in: query + description: >- + Key/value pairs of query parameters to pass to the OAuth provider. + Only applicable when using OAuth connections. + schema: + additionalProperties: + type: string + example: + hd: example.com + access_type: offline + type: object + - name: client_id + required: true + in: query + schema: + type: string + example: client_01HZBC6N1EB1ZY7KG32X + description: The unique identifier of the WorkOS environment client. + - name: domain + required: false + in: query + deprecated: true + schema: + type: string + example: example.com + description: >- + Deprecated. Use `connection` or `organization` instead. Used to + initiate SSO for a connection by domain. The domain must be + associated with a connection in your WorkOS environment. + - name: provider + required: false + in: query + schema: + type: string + enum: + - AppleOAuth + - BitbucketOAuth + - GitHubOAuth + - GitLabOAuth + - GoogleOAuth + - IntuitOAuth + - LinkedInOAuth - MicrosoftOAuth + - SalesforceOAuth + - SlackOAuth + - VercelMarketplaceOAuth + - VercelOAuth + - XeroOAuth example: GoogleOAuth - description: >- - Used to initiate OAuth authentication with Google, Microsoft, - GitHub, or Apple. + description: Used to initiate OAuth authentication with various providers. - name: redirect_uri required: true in: query @@ -9075,6 +10048,16 @@ paths: type: string description: The authorization code received from the redirect. example: vBqZKaPpsnJlPfXiDqN7b6VTz + code_verifier: + type: string + description: >- + The PKCE code verifier used to derive the code challenge + passed to the authorization URL. + example: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk + invitation_token: + type: string + description: An invitation token to accept during authentication. + example: inv_tok_01HXYZ123456789ABCDEFGHIJ ip_address: type: string description: The IP address of the user's request. @@ -10177,9 +11160,18 @@ paths: enum: - authkit - AppleOAuth + - BitbucketOAuth - GitHubOAuth + - GitLabOAuth - GoogleOAuth + - IntuitOAuth + - LinkedInOAuth - MicrosoftOAuth + - SalesforceOAuth + - SlackOAuth + - VercelMarketplaceOAuth + - VercelOAuth + - XeroOAuth example: GoogleOAuth description: >- The OAuth provider to authenticate with (e.g., GoogleOAuth, @@ -10875,6 +11867,16 @@ paths: The ID of the user who accepted the invitation, once accepted. example: user_01E4ZCR3C56J083X43JQXF3JK5 + role_slug: + type: + - string + - 'null' + description: >- + Slug of the role the invitee will be assigned on + acceptance. Reflects the current role on the invitee's + organization membership. null when the invitation has no + associated organization. + example: admin created_at: format: date-time type: string @@ -10905,6 +11907,7 @@ paths: - organization_id - inviter_user_id - accepted_user_id + - role_slug - created_at - updated_at - token @@ -11137,6 +12140,16 @@ paths: The ID of the user who accepted the invitation, once accepted. example: null + role_slug: + type: + - string + - 'null' + description: >- + Slug of the role the invitee will be assigned on + acceptance. Reflects the current role on the invitee's + organization membership. null when the invitation has no + associated organization. + example: admin created_at: format: date-time type: string @@ -11167,6 +12180,7 @@ paths: - organization_id - inviter_user_id - accepted_user_id + - role_slug - created_at - updated_at - token @@ -11791,6 +12805,14 @@ paths: - role_slug tags: - user-management.organization-membership + x-mutually-exclusive-body-groups: &ref_9 + role: + optional: true + variants: + single: + - role_slug + multiple: + - role_slugs /user_management/organization_memberships/{id}: get: operationId: UserlandUserOrganizationMembershipsController_get @@ -11967,6 +12989,14 @@ paths: - role_slug tags: - user-management.organization-membership + x-mutually-exclusive-body-groups: &ref_10 + role: + optional: true + variants: + single: + - role_slug + multiple: + - role_slugs /user_management/organization_memberships/{id}/deactivate: put: operationId: UserlandUserOrganizationMembershipsController_deactivate @@ -12225,6 +13255,94 @@ paths: - message tags: - user-management.organization-membership + /user_management/organization_memberships/{omId}/groups: + get: + operationId: OrganizationMembershipGroupsController_listGroups + summary: List groups + description: Get a list of groups that an organization membership belongs to. + parameters: + - name: omId + required: true + in: path + description: Unique identifier of the Organization Membership. + schema: + type: string + example: om_01HXYZ123456789ABCDEFGHIJ + - name: before + required: false + in: query + description: >- + An object ID that defines your place in the list. When the ID is not + present, you are at the end of the list. For example, if you make a + list request and receive 100 objects, ending with `"obj_123"`, your + subsequent call can include `before="obj_123"` to fetch a new batch + of objects before `"obj_123"`. + schema: + example: xxx_01HXYZ123456789ABCDEFGHIJ + type: string + - name: after + required: false + in: query + description: >- + An object ID that defines your place in the list. When the ID is not + present, you are at the end of the list. For example, if you make a + list request and receive 100 objects, ending with `"obj_123"`, your + subsequent call can include `after="obj_123"` to fetch a new batch + of objects after `"obj_123"`. + schema: + example: xxx_01HXYZ987654321KJIHGFEDCBA + type: string + - name: limit + required: false + in: query + description: >- + Upper limit on the number of objects to return, between `1` and + `100`. + schema: + minimum: 1 + maximum: 100 + default: 10 + example: 10 + type: integer + - name: order + required: false + in: query + description: >- + Order the results by the creation time. Supported values are `"asc"` + (ascending), `"desc"` (descending), and `"normal"` (descending with + reversed cursor semantics where `before` fetches older records and + `after` fetches newer records). Defaults to descending. + schema: + default: desc + example: desc + enum: + - normal + - desc + - asc + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GroupList' + '404': + description: Not Found + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: 'Organization not found: ''org_01EHQMYV6MBK39QC5PZXHY59C3''.' + required: + - message + tags: + - user-management.organization-membership.groups + x-feature-flag: user-groups-enabled /user_management/password_reset: post: operationId: UserlandUsersController_createPasswordResetToken @@ -12927,6 +14045,15 @@ paths: - message tags: - user-management.users + x-mutually-exclusive-body-groups: &ref_11 + password: + optional: true + variants: + plaintext: + - password + hashed: + - password_hash + - password_hash_type /user_management/users/external_id/{external_id}: get: operationId: UserlandUsersController_getByExternalId @@ -13193,6 +14320,15 @@ paths: - message tags: - user-management.users + x-mutually-exclusive-body-groups: &ref_14 + password: + optional: true + variants: + plaintext: + - password + hashed: + - password_hash + - password_hash_type get: operationId: UserlandUsersController_get summary: Get a user @@ -15171,6 +16307,8 @@ tags: description: Manage feature flags. - name: feature-flags.targets description: Manage feature flag targets. + - name: groups + description: Organize and manage user groups within organizations. - name: multi-factor-auth description: Multi-factor authentication factor management. - name: multi-factor-auth.challenges @@ -15207,6 +16345,8 @@ tags: description: Multi-factor authentication endpoints. - name: user-management.organization-membership description: Manage user organization memberships. + - name: user-management.organization-membership.groups + description: Manage groups for a user organization membership. - name: user-management.redirect-uris description: Manage redirect URIs. - name: user-management.session-tokens @@ -15383,7 +16523,7 @@ components: example: An application for managing user access scopes: description: The OAuth scopes granted to the application. - example: &ref_0 + example: &ref_1 - openid - profile - email @@ -15445,7 +16585,7 @@ components: example: An application for managing user access scopes: description: The OAuth scopes granted to the application. - example: *ref_0 + example: *ref_1 type: - array - 'null' @@ -15518,7 +16658,7 @@ components: owner: user_01GBTCQ2 maxProperties: 50 additionalProperties: false - patternProperties: &ref_1 + patternProperties: &ref_2 ^[a-zA-Z0-9_-]{0,40}$: anyOf: - type: string @@ -15550,7 +16690,7 @@ components: owner: user_01GBTCQ2 maxProperties: 50 additionalProperties: false - patternProperties: *ref_1 + patternProperties: *ref_2 required: - id - type @@ -15597,7 +16737,7 @@ components: owner: user_01GBTCQ2 maxProperties: 50 additionalProperties: false - patternProperties: *ref_1 + patternProperties: *ref_2 version: type: integer description: What schema version the event is associated with. @@ -15735,85 +16875,195 @@ components: type: object properties: transactionId: - type: string - required: - - targets - ChallengeAuthenticationFactorDto: - type: object - properties: - sms_template: - type: string - description: >- - A custom template for the SMS message. Use the {{code}} placeholder - to include the verification code. - example: Your verification code is {{code}}. - CheckAuthorizationDto: - type: object - properties: - permission_slug: - type: string - description: The slug of the permission to check. - example: posts:create - resource_id: - type: string - description: The ID of the resource. - example: resource_01HXYZ123456789ABCDEFGHIJ - resource_external_id: - type: string - description: The external ID of the resource. - example: my-custom-id - resource_type_slug: - type: string - description: The slug of the resource type. - example: document - required: - - permission_slug - AssignRoleDto: - type: object - properties: - role_slug: - type: string - description: The slug of the role to assign. - example: editor - resource_id: - type: string - description: >- - The ID of the resource. Use either this or `resource_external_id` - and `resource_type_slug`. - example: authz_resource_01HXYZ123456789ABCDEFGH - resource_external_id: - type: string - description: The external ID of the resource. Requires `resource_type_slug`. - example: project-ext-456 - resource_type_slug: - type: string - description: The resource type slug. Required with `resource_external_id`. - example: project + type: string required: - - role_slug - RemoveRoleDto: + - targets + ChallengeAuthenticationFactorDto: type: object properties: - role_slug: - type: string - description: The slug of the role to remove. - example: editor - resource_id: + sms_template: type: string description: >- - The ID of the resource. Use either this or `resource_external_id` - and `resource_type_slug`. - example: authz_resource_01HXYZ123456789ABCDEFGH - resource_external_id: - type: string - description: The external ID of the resource. Requires `resource_type_slug`. - example: external_01HXYZ123456789ABCDEFGH - resource_type_slug: - type: string - description: The resource type slug. Required with `resource_external_id`. - example: project - required: - - role_slug + A custom template for the SMS message. Use the {{code}} placeholder + to include the verification code. + example: Your verification code is {{code}}. + CheckAuthorizationDto: + allOf: + - type: object + properties: + permission_slug: + type: string + description: The slug of the permission to check. + example: posts:create + required: + - permission_slug + - oneOf: + - type: object + properties: + resource_id: + type: string + description: >- + The ID of the resource. Mutually exclusive with + `resource_external_id` and `resource_type_slug`. + example: resource_01HXYZ123456789ABCDEFGHIJ + required: + - resource_id + not: + anyOf: + - properties: + resource_external_id: + x-exclude-from-lint: true + required: + - resource_external_id + - properties: + resource_type_slug: + x-exclude-from-lint: true + required: + - resource_type_slug + - type: object + properties: + resource_external_id: + type: string + description: >- + The external ID of the resource. Required with + `resource_type_slug`. Mutually exclusive with `resource_id`. + example: my-custom-id + resource_type_slug: + type: string + description: >- + The slug of the resource type. Required with + `resource_external_id`. Mutually exclusive with + `resource_id`. + example: document + required: + - resource_external_id + - resource_type_slug + not: + anyOf: + - properties: + resource_id: + x-exclude-from-lint: true + required: + - resource_id + x-mutually-exclusive-body-groups: *ref_3 + AssignRoleDto: + allOf: + - type: object + properties: + role_slug: + type: string + description: The slug of the role to assign. + example: editor + required: + - role_slug + - oneOf: + - type: object + properties: + resource_id: + type: string + description: >- + The ID of the resource. Mutually exclusive with + `resource_external_id` and `resource_type_slug`. + example: authz_resource_01HXYZ123456789ABCDEFGH + required: + - resource_id + not: + anyOf: + - properties: + resource_external_id: + x-exclude-from-lint: true + required: + - resource_external_id + - properties: + resource_type_slug: + x-exclude-from-lint: true + required: + - resource_type_slug + - type: object + properties: + resource_external_id: + type: string + description: >- + The external ID of the resource. Required with + `resource_type_slug`. Mutually exclusive with `resource_id`. + example: project-ext-456 + resource_type_slug: + type: string + description: >- + The resource type slug. Required with + `resource_external_id`. Mutually exclusive with + `resource_id`. + example: project + required: + - resource_external_id + - resource_type_slug + not: + anyOf: + - properties: + resource_id: + x-exclude-from-lint: true + required: + - resource_id + x-mutually-exclusive-body-groups: *ref_4 + RemoveRoleDto: + allOf: + - type: object + properties: + role_slug: + type: string + description: The slug of the role to remove. + example: editor + required: + - role_slug + - oneOf: + - type: object + properties: + resource_id: + type: string + description: >- + The ID of the resource. Mutually exclusive with + `resource_external_id` and `resource_type_slug`. + example: authz_resource_01HXYZ123456789ABCDEFGH + required: + - resource_id + not: + anyOf: + - properties: + resource_external_id: + x-exclude-from-lint: true + required: + - resource_external_id + - properties: + resource_type_slug: + x-exclude-from-lint: true + required: + - resource_type_slug + - type: object + properties: + resource_external_id: + type: string + description: >- + The external ID of the resource. Required with + `resource_type_slug`. Mutually exclusive with `resource_id`. + example: external_01HXYZ123456789ABCDEFGH + resource_type_slug: + type: string + description: >- + The resource type slug. Required with + `resource_external_id`. Mutually exclusive with + `resource_id`. + example: project + required: + - resource_external_id + - resource_type_slug + not: + anyOf: + - properties: + resource_id: + x-exclude-from-lint: true + required: + - resource_id + x-mutually-exclusive-body-groups: *ref_5 SetRolePermissionsDto: type: object properties: @@ -15887,175 +17137,332 @@ components: CreateAuthorizationPermissionDto: type: object properties: - slug: - type: string - maxLength: 48 - description: >- - A unique key to reference the permission. Must be lowercase and - contain only letters, numbers, hyphens, underscores, colons, - periods, and asterisks. - example: documents:read - name: - type: string - maxLength: 48 - description: A descriptive name for the Permission. - example: View Documents - description: - type: - - string - - 'null' - maxLength: 150 - description: An optional description of the Permission. - example: Allows viewing document contents - resource_type_slug: + slug: + type: string + maxLength: 48 + description: >- + A unique key to reference the permission. Must be lowercase and + contain only letters, numbers, hyphens, underscores, colons, + periods, and asterisks. + example: documents:read + name: + type: string + maxLength: 48 + description: A descriptive name for the Permission. + example: View Documents + description: + type: + - string + - 'null' + maxLength: 150 + description: An optional description of the Permission. + example: Allows viewing document contents + resource_type_slug: + type: string + maxLength: 48 + description: The slug of the resource type this permission is scoped to. + example: document + required: + - slug + - name + UpdateAuthorizationPermissionDto: + type: object + properties: + name: + type: string + maxLength: 48 + description: A descriptive name for the Permission. + example: View Documents + description: + type: + - string + - 'null' + maxLength: 150 + description: An optional description of the Permission. + example: Allows viewing document contents + CreateRoleDto: + type: object + properties: + slug: + type: string + maxLength: 48 + description: A unique slug for the role. + example: editor + name: + type: string + maxLength: 48 + description: A descriptive name for the role. + example: Editor + description: + type: + - string + - 'null' + maxLength: 150 + description: An optional description of the role. + example: Can edit resources + resource_type_slug: + type: string + maxLength: 48 + description: The slug of the resource type the role is scoped to. + example: organization + required: + - slug + - name + UpdateRoleDto: + type: object + properties: + name: + type: string + maxLength: 48 + description: A descriptive name for the role. + example: Super Administrator + description: + type: + - string + - 'null' + maxLength: 150 + description: An optional description of the role. + example: Full administrative access to all resources + UpdateAuthorizationResourceDto: + allOf: + - type: object + properties: + name: + type: string + maxLength: 48 + description: A display name for the resource. + example: Updated Name + description: + type: + - string + - 'null' + maxLength: 150 + description: An optional description of the resource. + example: Updated description + - oneOf: + - type: object + not: + anyOf: + - properties: + parent_resource_id: + x-exclude-from-lint: true + required: + - parent_resource_id + - properties: + parent_resource_external_id: + x-exclude-from-lint: true + required: + - parent_resource_external_id + - properties: + parent_resource_type_slug: + x-exclude-from-lint: true + required: + - parent_resource_type_slug + - type: object + properties: + parent_resource_id: + type: string + description: >- + The ID of the parent resource. Mutually exclusive with + `parent_resource_external_id` and + `parent_resource_type_slug`. + example: authz_resource_01HXYZ123456789ABCDEFGHIJ + required: + - parent_resource_id + not: + anyOf: + - properties: + parent_resource_external_id: + x-exclude-from-lint: true + required: + - parent_resource_external_id + - properties: + parent_resource_type_slug: + x-exclude-from-lint: true + required: + - parent_resource_type_slug + - type: object + properties: + parent_resource_external_id: + type: string + description: >- + The external ID of the parent resource. Required with + `parent_resource_type_slug`. Mutually exclusive with + `parent_resource_id`. + example: parent-workspace-01 + parent_resource_type_slug: + type: string + description: >- + The resource type slug of the parent resource. Required with + `parent_resource_external_id`. Mutually exclusive with + `parent_resource_id`. + example: workspace + required: + - parent_resource_external_id + - parent_resource_type_slug + not: + anyOf: + - properties: + parent_resource_id: + x-exclude-from-lint: true + required: + - parent_resource_id + x-mutually-exclusive-body-groups: *ref_0 + CreateAuthorizationResourceDto: + allOf: + - type: object + properties: + external_id: + type: string + maxLength: 128 + description: An external identifier for the resource. + example: my-workspace-01 + name: + type: string + maxLength: 48 + description: A display name for the resource. + example: Acme Workspace + description: + type: + - string + - 'null' + maxLength: 150 + description: An optional description of the resource. + example: Primary workspace for the Acme team + resource_type_slug: + type: string + description: The slug of the resource type. + example: workspace + organization_id: + type: string + description: The ID of the organization this resource belongs to. + example: org_01EHQMYV6MBK39QC5PZXHY59C3 + required: + - external_id + - name + - resource_type_slug + - organization_id + - oneOf: + - type: object + not: + anyOf: + - properties: + parent_resource_id: + x-exclude-from-lint: true + required: + - parent_resource_id + - properties: + parent_resource_external_id: + x-exclude-from-lint: true + required: + - parent_resource_external_id + - properties: + parent_resource_type_slug: + x-exclude-from-lint: true + required: + - parent_resource_type_slug + - type: object + properties: + parent_resource_id: + type: + - string + - 'null' + description: >- + The ID of the parent resource. Mutually exclusive with + `parent_resource_external_id` and + `parent_resource_type_slug`. + example: authz_resource_01HXYZ123456789ABCDEFGHIJ + required: + - parent_resource_id + not: + anyOf: + - properties: + parent_resource_external_id: + x-exclude-from-lint: true + required: + - parent_resource_external_id + - properties: + parent_resource_type_slug: + x-exclude-from-lint: true + required: + - parent_resource_type_slug + - type: object + properties: + parent_resource_external_id: + type: string + description: >- + The external ID of the parent resource. Required with + `parent_resource_type_slug`. Mutually exclusive with + `parent_resource_id`. + example: parent-workspace-01 + parent_resource_type_slug: + type: string + description: >- + The resource type slug of the parent resource. Required with + `parent_resource_external_id`. Mutually exclusive with + `parent_resource_id`. + example: workspace + required: + - parent_resource_external_id + - parent_resource_type_slug + not: + anyOf: + - properties: + parent_resource_id: + x-exclude-from-lint: true + required: + - parent_resource_id + x-mutually-exclusive-body-groups: *ref_6 + CreateCorsOriginDto: + type: object + properties: + origin: type: string - maxLength: 48 - description: The slug of the resource type this permission is scoped to. - example: document + description: The origin URL to allow for CORS requests. + example: https://example.com required: - - slug - - name - UpdateAuthorizationPermissionDto: + - origin + CreateGroupMembershipDto: type: object properties: - name: + organization_membership_id: type: string - maxLength: 48 - description: A descriptive name for the Permission. - example: View Documents - description: - type: - - string - - 'null' - maxLength: 150 - description: An optional description of the Permission. - example: Allows viewing document contents - CreateRoleDto: + description: The ID of the Organization Membership to add to the group. + example: om_01HXYZ123456789ABCDEFGHIJ + required: + - organization_membership_id + CreateGroupDto: type: object properties: - slug: - type: string - maxLength: 48 - description: A unique slug for the role. - example: editor name: type: string - maxLength: 48 - description: A descriptive name for the role. - example: Editor + maxLength: 256 + description: The name of the Group. + example: Engineering description: type: - string - 'null' maxLength: 150 - description: An optional description of the role. - example: Can edit resources - resource_type_slug: - type: string - maxLength: 48 - description: The slug of the resource type the role is scoped to. - example: organization + description: An optional description of the Group. + example: The engineering team required: - - slug - name - UpdateRoleDto: - type: object - properties: - name: - type: string - maxLength: 48 - description: A descriptive name for the role. - example: Super Administrator - description: - type: - - string - - 'null' - maxLength: 150 - description: An optional description of the role. - example: Full administrative access to all resources - UpdateAuthorizationResourceDto: - type: object - properties: - name: - type: string - maxLength: 48 - description: A display name for the resource. - example: Updated Name - description: - type: - - string - - 'null' - maxLength: 150 - description: An optional description of the resource. - example: Updated description - parent_resource_id: - type: string - description: The ID of the parent resource. - example: authz_resource_01HXYZ123456789ABCDEFGHIJ - parent_resource_external_id: - type: string - description: The external ID of the parent resource. - example: parent-workspace-01 - parent_resource_type_slug: - type: string - description: The resource type slug of the parent resource. - example: workspace - CreateAuthorizationResourceDto: + UpdateGroupDto: type: object properties: - external_id: - type: string - maxLength: 128 - description: An external identifier for the resource. - example: my-workspace-01 name: type: string - maxLength: 48 - description: A display name for the resource. - example: Acme Workspace + maxLength: 256 + description: The name of the Group. + example: Engineering description: type: - string - 'null' maxLength: 150 - description: An optional description of the resource. - example: Primary workspace for the Acme team - resource_type_slug: - type: string - description: The slug of the resource type. - example: workspace - organization_id: - type: string - description: The ID of the organization this resource belongs to. - example: org_01EHQMYV6MBK39QC5PZXHY59C3 - parent_resource_id: - type: - - string - - 'null' - description: The ID of the parent resource. - example: authz_resource_01HXYZ123456789ABCDEFGHIJ - parent_resource_external_id: - type: string - description: The external ID of the parent resource. - example: parent-workspace-01 - parent_resource_type_slug: - type: string - description: The resource type slug of the parent resource. - example: workspace - required: - - external_id - - name - - resource_type_slug - - organization_id - CreateCorsOriginDto: - type: object - properties: - origin: - type: string - description: The origin URL to allow for CORS requests. - example: https://example.com - required: - - origin + description: An optional description of the Group. + example: The engineering team UpdateJwtTemplateDto: type: object properties: @@ -16146,11 +17553,11 @@ components: type: - object - 'null' - additionalProperties: &ref_2 + additionalProperties: &ref_7 type: string maxLength: 600 maxProperties: 50 - example: &ref_3 + example: &ref_8 tier: diamond description: >- Object containing [metadata](/authkit/metadata) key/value pairs @@ -16204,9 +17611,9 @@ components: type: - object - 'null' - additionalProperties: *ref_2 + additionalProperties: *ref_7 maxProperties: 50 - example: *ref_3 + example: *ref_8 description: >- Object containing [metadata](/authkit/metadata) key/value pairs associated with the Organization. @@ -16231,14 +17638,25 @@ components: description: The SSO provider type to configure. example: GoogleSAML const: GoogleSAML + DomainVerificationIntentOptions: + type: object + properties: + domain_name: + type: string + description: >- + The domain name to verify. When provided, the domain verification + flow will skip the domain entry form and go directly to the + verification step. + example: example.com IntentOptions: type: object properties: sso: description: SSO-specific options for the Admin Portal. $ref: '#/components/schemas/SsoIntentOptions' - required: - - sso + domain_verification: + description: Domain verification-specific options for the Admin Portal. + $ref: '#/components/schemas/DomainVerificationIntentOptions' GenerateLinkDto: type: object properties: @@ -16286,12 +17704,12 @@ components: intent_options: description: Options to configure the Admin Portal based on the intent. $ref: '#/components/schemas/IntentOptions' - admin_emails: + it_contact_emails: description: >- - The email addresses of the IT admins to grant access to the Admin + The email addresses of the IT contacts to grant access to the Admin Portal for the given organization. Accepts up to 20 emails. example: - - admin@example.com + - it-contact@example.com maxItems: 20 items: type: string @@ -16580,200 +17998,365 @@ components: [supported locales](/authkit/hosted-ui/localization). example: en CreateUserlandUserOrganizationMembershipDto: - type: object - properties: - user_id: - type: string - description: The ID of the [user](/reference/authkit/user). - example: user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E - organization_id: - type: string - description: >- - The ID of the [organization](/reference/organization) which the user - belongs to. - example: org_01E4ZCR3C56J083X43JQXF3JK5 - role_slug: - type: string - description: >- - A single role identifier. Defaults to `member` or the explicit - default role. Mutually exclusive with `role_slugs`. - example: admin - x-mutually-exclusive-with: - - role_slugs - role_slugs: - description: >- - An array of role identifiers. Limited to one role when Multiple - Roles is disabled. Mutually exclusive with `role_slug`. - example: - - admin - x-mutually-exclusive-with: - - role_slug - type: array - items: - type: string - required: - - user_id - - organization_id - UpdateUserlandUserOrganizationMembershipDto: - type: object - properties: - role_slug: - type: string - description: >- - A single role identifier. Defaults to `member` or the explicit - default role. Mutually exclusive with `role_slugs`. - example: admin - x-mutually-exclusive-with: - - role_slugs - role_slugs: - description: >- - An array of role identifiers. Limited to one role when Multiple - Roles is disabled. Mutually exclusive with `role_slug`. - example: - - admin - x-mutually-exclusive-with: - - role_slug - type: array - items: - type: string - CreateUserlandUserDto: - type: object - properties: - email: - type: string - description: The email address of the user. - example: marcelina.davis@example.com - password: - type: - - string - - 'null' - description: >- - The password to set for the user. Mutually exclusive with - `password_hash` and `password_hash_type`. - example: strong_password_123! - x-mutually-exclusive-with: - - password_hash - - password_hash_type - password_hash: - type: string - description: >- - The hashed password to set for the user. Mutually exclusive with - `password`. - example: $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy - x-mutually-exclusive-with: - - password - password_hash_type: - type: string - enum: &ref_4 - - bcrypt - - firebase-scrypt - - ssha - - scrypt - - pbkdf2 - - argon2 - description: >- - The algorithm originally used to hash the password, used when - providing a `password_hash`. - example: bcrypt - first_name: - type: - - string - - 'null' - description: The first name of the user. - example: Marcelina - last_name: - type: - - string - - 'null' - description: The last name of the user. - example: Davis - email_verified: - type: - - boolean - - 'null' - description: Whether the user's email has been verified. - example: true - metadata: - type: - - object - - 'null' - additionalProperties: *ref_2 - maxProperties: 50 - example: &ref_5 - timezone: America/New_York - description: Object containing metadata key/value pairs associated with the user. - propertyNames: - maxLength: 40 - external_id: - type: - - string - - 'null' - maxLength: 128 - description: The external ID of the user. - example: f1ffa2b2-c20b-4d39-be5c-212726e11222 - required: - - email + allOf: + - type: object + properties: + user_id: + type: string + description: The ID of the [user](/reference/authkit/user). + example: user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E + organization_id: + type: string + description: >- + The ID of the [organization](/reference/organization) which the + user belongs to. + example: org_01E4ZCR3C56J083X43JQXF3JK5 + required: + - user_id + - organization_id + - oneOf: + - type: object + not: + anyOf: + - properties: + role_slug: + x-exclude-from-lint: true + required: + - role_slug + - properties: + role_slugs: + x-exclude-from-lint: true + required: + - role_slugs + - type: object + properties: + role_slug: + type: string + description: >- + A single role identifier. Defaults to `member` or the + explicit default role. Mutually exclusive with `role_slugs`. + example: admin + required: + - role_slug + not: + anyOf: + - properties: + role_slugs: + x-exclude-from-lint: true + required: + - role_slugs + - type: object + properties: + role_slugs: + description: >- + An array of role identifiers. Limited to one role when + Multiple Roles is disabled. Mutually exclusive with + `role_slug`. + example: + - admin + type: array + items: + type: string + required: + - role_slugs + not: + anyOf: + - properties: + role_slug: + x-exclude-from-lint: true + required: + - role_slug + x-mutually-exclusive-body-groups: *ref_9 + UpdateUserlandUserOrganizationMembershipDto: + x-mutually-exclusive-body-groups: *ref_10 + oneOf: + - type: object + not: + anyOf: + - properties: + role_slug: + x-exclude-from-lint: true + required: + - role_slug + - properties: + role_slugs: + x-exclude-from-lint: true + required: + - role_slugs + - type: object + properties: + role_slug: + type: string + description: >- + A single role identifier. Defaults to `member` or the explicit + default role. Mutually exclusive with `role_slugs`. + example: admin + required: + - role_slug + not: + anyOf: + - properties: + role_slugs: + x-exclude-from-lint: true + required: + - role_slugs + - type: object + properties: + role_slugs: + description: >- + An array of role identifiers. Limited to one role when Multiple + Roles is disabled. Mutually exclusive with `role_slug`. + example: + - admin + type: array + items: + type: string + required: + - role_slugs + not: + anyOf: + - properties: + role_slug: + x-exclude-from-lint: true + required: + - role_slug + CreateUserlandUserDto: + allOf: + - type: object + properties: + email: + type: string + description: The email address of the user. + example: marcelina.davis@example.com + first_name: + type: + - string + - 'null' + description: The first name of the user. + example: Marcelina + last_name: + type: + - string + - 'null' + description: The last name of the user. + example: Davis + email_verified: + type: + - boolean + - 'null' + description: Whether the user's email has been verified. + example: true + metadata: + type: + - object + - 'null' + additionalProperties: *ref_7 + maxProperties: 50 + example: &ref_12 + timezone: America/New_York + description: >- + Object containing metadata key/value pairs associated with the + user. + propertyNames: + maxLength: 40 + external_id: + type: + - string + - 'null' + maxLength: 128 + description: The external ID of the user. + example: f1ffa2b2-c20b-4d39-be5c-212726e11222 + required: + - email + - oneOf: + - type: object + not: + anyOf: + - properties: + password: + x-exclude-from-lint: true + required: + - password + - properties: + password_hash: + x-exclude-from-lint: true + required: + - password_hash + - properties: + password_hash_type: + x-exclude-from-lint: true + required: + - password_hash_type + - type: object + properties: + password: + type: + - string + - 'null' + description: >- + The password to set for the user. Mutually exclusive with + `password_hash` and `password_hash_type`. + example: strong_password_123! + required: + - password + not: + anyOf: + - properties: + password_hash: + x-exclude-from-lint: true + required: + - password_hash + - properties: + password_hash_type: + x-exclude-from-lint: true + required: + - password_hash_type + - type: object + properties: + password_hash: + type: string + description: >- + The hashed password to set for the user. Required with + `password_hash_type`. Mutually exclusive with `password`. + example: $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy + password_hash_type: + type: string + enum: &ref_13 + - bcrypt + - firebase-scrypt + - ssha + - scrypt + - pbkdf2 + - argon2 + description: >- + The algorithm originally used to hash the password, used + when providing a `password_hash`. Required with + `password_hash`. Mutually exclusive with `password`. + example: bcrypt + required: + - password_hash + - password_hash_type + not: + anyOf: + - properties: + password: + x-exclude-from-lint: true + required: + - password + x-mutually-exclusive-body-groups: *ref_11 UpdateUserlandUserDto: - type: object - properties: - email: - type: string - description: The email address of the user. - example: marcelina.davis@example.com - first_name: - type: string - description: The first name of the user. - example: Marcelina - last_name: - type: string - description: The last name of the user. - example: Davis - email_verified: - type: boolean - description: Whether the user's email has been verified. - example: true - password: - type: string - description: The password to set for the user. - example: strong_password_123! - password_hash: - type: string - description: >- - The hashed password to set for the user. Mutually exclusive with - `password`. - example: $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy - x-mutually-exclusive-with: - - password - password_hash_type: - type: string - enum: *ref_4 - description: >- - The algorithm originally used to hash the password, used when - providing a `password_hash`. - example: bcrypt - metadata: - type: - - object - - 'null' - additionalProperties: *ref_2 - maxProperties: 50 - example: *ref_5 - description: Object containing metadata key/value pairs associated with the user. - propertyNames: - maxLength: 40 - external_id: - type: - - string - - 'null' - maxLength: 128 - description: The external ID of the user. - example: f1ffa2b2-c20b-4d39-be5c-212726e11222 - locale: - type: - - string - - 'null' - description: The user's preferred locale. - example: en-US + allOf: + - type: object + properties: + email: + type: string + description: The email address of the user. + example: marcelina.davis@example.com + first_name: + type: string + description: The first name of the user. + example: Marcelina + last_name: + type: string + description: The last name of the user. + example: Davis + email_verified: + type: boolean + description: Whether the user's email has been verified. + example: true + metadata: + type: + - object + - 'null' + additionalProperties: *ref_7 + maxProperties: 50 + example: *ref_12 + description: >- + Object containing metadata key/value pairs associated with the + user. + propertyNames: + maxLength: 40 + external_id: + type: + - string + - 'null' + maxLength: 128 + description: The external ID of the user. + example: f1ffa2b2-c20b-4d39-be5c-212726e11222 + locale: + type: + - string + - 'null' + description: The user's preferred locale. + example: en-US + - oneOf: + - type: object + not: + anyOf: + - properties: + password: + x-exclude-from-lint: true + required: + - password + - properties: + password_hash: + x-exclude-from-lint: true + required: + - password_hash + - properties: + password_hash_type: + x-exclude-from-lint: true + required: + - password_hash_type + - type: object + properties: + password: + type: string + description: >- + The password to set for the user. Mutually exclusive with + `password_hash` and `password_hash_type`. + example: strong_password_123! + required: + - password + not: + anyOf: + - properties: + password_hash: + x-exclude-from-lint: true + required: + - password_hash + - properties: + password_hash_type: + x-exclude-from-lint: true + required: + - password_hash_type + - type: object + properties: + password_hash: + type: string + description: >- + The hashed password to set for the user. Required with + `password_hash_type`. Mutually exclusive with `password`. + example: $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy + password_hash_type: + type: string + enum: *ref_13 + description: >- + The algorithm originally used to hash the password, used + when providing a `password_hash`. Required with + `password_hash`. Mutually exclusive with `password`. + example: bcrypt + required: + - password_hash + - password_hash_type + not: + anyOf: + - properties: + password: + x-exclude-from-lint: true + required: + - password + x-mutually-exclusive-body-groups: *ref_14 VerifyEmailAddressDto: type: object properties: @@ -16851,7 +18434,7 @@ components: description: The events that the Webhook Endpoint is subscribed to. items: type: string - enum: &ref_6 + enum: &ref_15 - authentication.email_verification_succeeded - authentication.magic_auth_failed - authentication.magic_auth_succeeded @@ -16885,6 +18468,11 @@ components: - dsync.user.deleted - dsync.user.updated - email_verification.created + - group.created + - group.deleted + - group.member_added + - group.member_removed + - group.updated - flag.created - flag.deleted - flag.updated @@ -16921,6 +18509,9 @@ components: - permission.updated - session.created - session.revoked + - waitlist_user.approved + - waitlist_user.created + - waitlist_user.denied example: - user.created - dsync.user.created @@ -16946,7 +18537,7 @@ components: description: The events that the Webhook Endpoint is subscribed to. items: type: string - enum: *ref_6 + enum: *ref_15 example: - user.created - dsync.user.created @@ -17316,6 +18907,12 @@ components: - openid - profile - email + oauth_resource: + type: string + description: >- + The OAuth resource associated with the authorized connect + application, if one was requested. + example: https://api.example.com/resource application: $ref: '#/components/schemas/ConnectApplication' required: @@ -17813,26 +19410,124 @@ components: example: Company website redesign project organization_id: type: string - description: The ID of the organization that owns the resource. - example: org_01EHZNVPK3SFK441A1RGBFSHRT - parent_resource_id: + description: The ID of the organization that owns the resource. + example: org_01EHZNVPK3SFK441A1RGBFSHRT + parent_resource_id: + type: + - string + - 'null' + description: The ID of the parent resource, if this resource is nested. + example: authz_resource_01HXYZ123456789ABCDEFGHIJ + id: + type: string + description: The unique ID of the Resource. + example: authz_resource_01HXYZ123456789ABCDEFGH + external_id: + type: string + description: An identifier you provide to reference the resource in your system. + example: proj-456 + resource_type_slug: + type: string + description: The slug of the resource type this resource belongs to. + example: project + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + updated_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + required: + - object + - name + - description + - organization_id + - parent_resource_id + - id + - external_id + - resource_type_slug + - created_at + - updated_at + AuthorizationResourceList: + type: object + properties: + object: + type: string + description: Indicates this is a list response. + const: list + data: + type: array + items: + $ref: '#/components/schemas/AuthorizationResource' + description: The list of records for the current page. + list_metadata: + type: object + properties: + before: + type: + - string + - 'null' + description: >- + An object ID that defines your place in the list. When the ID is + not present, you are at the start of the list. + example: authz_resource_01HXYZ123456789ABCDEFGHIJ + after: + type: + - string + - 'null' + description: >- + An object ID that defines your place in the list. When the ID is + not present, you are at the end of the list. + example: authz_resource_01HXYZ987654321KJIHGFEDCBA + required: + - before + - after + description: Pagination cursors for navigating between pages of results. + required: + - object + - data + - list_metadata + AuthorizationPermission: + type: object + properties: + object: + type: string + description: Distinguishes the Permission object. + const: permission + id: + type: string + description: Unique identifier of the Permission. + example: perm_01HXYZ123456789ABCDEFGHIJ + slug: + type: string + description: >- + A unique key to reference the permission. Must be lowercase and + contain only letters, numbers, hyphens, underscores, colons, + periods, and asterisks. + example: documents:read + name: + type: string + description: A descriptive name for the Permission. + example: View Documents + description: type: - string - 'null' - description: The ID of the parent resource, if this resource is nested. - example: authz_resource_01HXYZ123456789ABCDEFGHIJ - id: - type: string - description: The unique ID of the Resource. - example: authz_resource_01HXYZ123456789ABCDEFGH - external_id: - type: string - description: An identifier you provide to reference the resource in your system. - example: proj-456 + description: An optional description of the Permission. + example: Allows viewing document contents + system: + type: boolean + description: >- + Whether the permission is a system permission. System permissions + are managed by WorkOS and cannot be deleted. + example: false resource_type_slug: type: string - description: The slug of the resource type this resource belongs to. - example: project + description: The slug of the resource type associated with the permission. + example: workspace created_at: format: date-time type: string @@ -17845,16 +19540,15 @@ components: example: '2026-01-15T12:00:00.000Z' required: - object + - id + - slug - name - description - - organization_id - - parent_resource_id - - id - - external_id + - system - resource_type_slug - created_at - updated_at - AuthorizationResourceList: + AuthorizationPermissionList: type: object properties: object: @@ -17864,7 +19558,7 @@ components: data: type: array items: - $ref: '#/components/schemas/AuthorizationResource' + $ref: '#/components/schemas/AuthorizationPermission' description: The list of records for the current page. list_metadata: type: object @@ -17876,7 +19570,7 @@ components: description: >- An object ID that defines your place in the list. When the ID is not present, you are at the start of the list. - example: authz_resource_01HXYZ123456789ABCDEFGHIJ + example: perm_01HXYZ123456789ABCDEFGHIJ after: type: - string @@ -17884,7 +19578,7 @@ components: description: >- An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. - example: authz_resource_01HXYZ987654321KJIHGFEDCBA + example: perm_01HXYZ987654321KJIHGFEDCBA required: - before - after @@ -18026,7 +19720,9 @@ components: enum: - EnvironmentRole - OrganizationRole - description: Whether the role is scoped to the environment or an organization. + description: >- + Whether the role is scoped to the environment or an organization + (custom role). example: EnvironmentRole resource_type_slug: type: string @@ -18076,103 +19772,6 @@ components: required: - object - data - AuthorizationPermission: - type: object - properties: - object: - type: string - description: Distinguishes the Permission object. - const: permission - id: - type: string - description: Unique identifier of the Permission. - example: perm_01HXYZ123456789ABCDEFGHIJ - slug: - type: string - description: >- - A unique key to reference the permission. Must be lowercase and - contain only letters, numbers, hyphens, underscores, colons, - periods, and asterisks. - example: documents:read - name: - type: string - description: A descriptive name for the Permission. - example: View Documents - description: - type: - - string - - 'null' - description: An optional description of the Permission. - example: Allows viewing document contents - system: - type: boolean - description: >- - Whether the permission is a system permission. System permissions - are managed by WorkOS and cannot be deleted. - example: false - resource_type_slug: - type: string - description: The slug of the resource type associated with the permission. - example: workspace - created_at: - format: date-time - type: string - description: An ISO 8601 timestamp. - example: '2026-01-15T12:00:00.000Z' - updated_at: - format: date-time - type: string - description: An ISO 8601 timestamp. - example: '2026-01-15T12:00:00.000Z' - required: - - object - - id - - slug - - name - - description - - system - - resource_type_slug - - created_at - - updated_at - AuthorizationPermissionList: - type: object - properties: - object: - type: string - description: Indicates this is a list response. - const: list - data: - type: array - items: - $ref: '#/components/schemas/AuthorizationPermission' - description: The list of records for the current page. - list_metadata: - type: object - properties: - before: - type: - - string - - 'null' - description: >- - An object ID that defines your place in the list. When the ID is - not present, you are at the start of the list. - example: perm_01HXYZ123456789ABCDEFGHIJ - after: - type: - - string - - 'null' - description: >- - An object ID that defines your place in the list. When the ID is - not present, you are at the end of the list. - example: perm_01HXYZ987654321KJIHGFEDCBA - required: - - before - - after - description: Pagination cursors for navigating between pages of results. - required: - - object - - data - - list_metadata UserlandUserOrganizationMembershipBaseList: type: object properties: @@ -18876,7 +20475,10 @@ components: type: array items: $ref: '#/components/schemas/DirectoryGroup' - description: The directory groups the user belongs to. + description: >- + The directory groups the user belongs to. Use the List Directory + Groups endpoint with a user filter instead. + deprecated: true required: - object - id @@ -18930,6 +20532,89 @@ components: - data - list_metadata - list_metadata + Group: + type: object + properties: + object: + type: string + description: The Group object. + example: group + const: group + id: + type: string + description: The unique ID of the Group. + example: group_01HXYZ123456789ABCDEFGHIJ + organization_id: + type: string + description: The ID of the Organization the Group belongs to. + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + name: + type: string + description: The name of the Group. + example: Engineering + description: + type: + - string + - 'null' + description: An optional description of the Group. + example: The engineering team + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + updated_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + required: + - object + - id + - organization_id + - name + - description + - created_at + - updated_at + GroupList: + type: object + properties: + object: + type: string + description: Indicates this is a list response. + const: list + data: + type: array + items: + $ref: '#/components/schemas/Group' + description: The list of records for the current page. + list_metadata: + type: object + properties: + before: + type: + - string + - 'null' + description: >- + An object ID that defines your place in the list. When the ID is + not present, you are at the start of the list. + example: group_01HXYZ123456789ABCDEFGHIJ + after: + type: + - string + - 'null' + description: >- + An object ID that defines your place in the list. When the ID is + not present, you are at the end of the list. + example: group_01HXYZ987654321KJIHGFEDCBA + required: + - before + - after + description: Pagination cursors for navigating between pages of results. + required: + - object + - data + - list_metadata EventContextActorDto: type: object properties: @@ -19094,7 +20779,7 @@ components: description: >- An object containing the custom attribute mapping for the Directory Provider. - example: &ref_9 + example: &ref_18 department: Engineering job_title: Software Engineer role: @@ -19215,7 +20900,57 @@ components: - last_sign_in_at - created_at - updated_at - description: The user object. + description: The user object. + WaitlistUser: + type: object + properties: + object: + type: string + description: Distinguishes the Waitlist User object. + const: waitlist_user + id: + type: string + description: The unique ID of the Waitlist User. + example: wl_user_01E4ZCR3C56J083X43JQXF3JK5 + email: + type: string + description: The email address of the Waitlist User. + example: marcelina.davis@example.com + state: + type: string + enum: + - pending + - approved + - denied + description: The state of the Waitlist User. + example: pending + approved_at: + format: date-time + type: + - string + - 'null' + description: >- + The timestamp when the Waitlist User was approved, or null if not + yet approved. + example: null + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + updated_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + required: + - object + - id + - email + - state + - approved_at + - created_at + - updated_at EventSchema: allOf: - type: object @@ -19236,7 +20971,7 @@ components: type: object additionalProperties: {} description: The event payload. - example: &ref_7 + example: &ref_16 id: directory_user_01E1JG7J09H96KYP8HM9B0G5SJ directory_id: directory_01ECAZ4NV9QMV47GW873HDCX74 organization_id: org_01EZTR6WYX1A0DSE2CYMGXQ24Y @@ -19271,11 +21006,11 @@ components: - data - created_at description: An event emitted by WorkOS. - example: &ref_14 + example: &ref_23 object: event id: event_01EHZNVPK3SFK441A1RGBFSHRT event: dsync.user.created - data: *ref_7 + data: *ref_16 created_at: '2021-06-25T19:07:33.155Z' context: {} - oneOf: @@ -19492,7 +21227,7 @@ components: items: type: string description: The permissions granted to the API key. - example: &ref_8 + example: &ref_17 - users:read - users:write created_at: @@ -19585,7 +21320,7 @@ components: items: type: string description: The permissions granted to the API key. - example: *ref_8 + example: *ref_17 created_at: type: string description: The timestamp when the API key was created. @@ -22363,7 +24098,7 @@ components: description: >- An object containing the custom attribute mapping for the Directory Provider. - example: *ref_9 + example: *ref_18 role: $ref: '#/components/schemas/SlimRole' roles: @@ -22553,7 +24288,7 @@ components: description: >- Labels assigned to the Feature Flag for categorizing and filtering. - example: &ref_10 + example: &ref_19 - reports enabled: type: boolean @@ -22713,7 +24448,7 @@ components: description: >- Labels assigned to the Feature Flag for categorizing and filtering. - example: *ref_10 + example: *ref_19 enabled: type: boolean description: >- @@ -22872,7 +24607,7 @@ components: description: >- Labels assigned to the Feature Flag for categorizing and filtering. - example: *ref_10 + example: *ref_19 enabled: type: boolean description: >- @@ -23151,7 +24886,7 @@ components: description: >- Labels assigned to the Feature Flag for categorizing and filtering. - example: *ref_10 + example: *ref_19 enabled: type: boolean description: >- @@ -23274,6 +25009,175 @@ components: - created_at - context - object + - type: object + properties: + id: + type: string + description: Unique identifier for the event. + example: event_01EHZNVPK3SFK441A1RGBFSHRT + event: + type: string + const: group.created + data: + $ref: '#/components/schemas/Group' + description: The event payload. + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + context: + $ref: '#/components/schemas/EventContextDto' + object: + type: string + description: Distinguishes the Event object. + const: event + required: + - id + - event + - data + - created_at + - object + - type: object + properties: + id: + type: string + description: Unique identifier for the event. + example: event_01EHZNVPK3SFK441A1RGBFSHRT + event: + type: string + const: group.deleted + data: + $ref: '#/components/schemas/Group' + description: The event payload. + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + context: + $ref: '#/components/schemas/EventContextDto' + object: + type: string + description: Distinguishes the Event object. + const: event + required: + - id + - event + - data + - created_at + - object + - type: object + properties: + id: + type: string + description: Unique identifier for the event. + example: event_01EHZNVPK3SFK441A1RGBFSHRT + event: + type: string + const: group.member_added + data: + type: object + properties: + group_id: + type: string + description: The ID of the Group. + example: group_01HXYZ123456789ABCDEFGHIJ + organization_membership_id: + type: string + description: The ID of the OrganizationMembership. + example: om_01EHWNCE74X7JSDV0X3SZ3KJNY + required: + - group_id + - organization_membership_id + description: The event payload. + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + context: + $ref: '#/components/schemas/EventContextDto' + object: + type: string + description: Distinguishes the Event object. + const: event + required: + - id + - event + - data + - created_at + - object + - type: object + properties: + id: + type: string + description: Unique identifier for the event. + example: event_01EHZNVPK3SFK441A1RGBFSHRT + event: + type: string + const: group.member_removed + data: + type: object + properties: + group_id: + type: string + description: The ID of the Group. + example: group_01HXYZ123456789ABCDEFGHIJ + organization_membership_id: + type: string + description: The ID of the OrganizationMembership. + example: om_01EHWNCE74X7JSDV0X3SZ3KJNY + required: + - group_id + - organization_membership_id + description: The event payload. + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + context: + $ref: '#/components/schemas/EventContextDto' + object: + type: string + description: Distinguishes the Event object. + const: event + required: + - id + - event + - data + - created_at + - object + - type: object + properties: + id: + type: string + description: Unique identifier for the event. + example: event_01EHZNVPK3SFK441A1RGBFSHRT + event: + type: string + const: group.updated + data: + $ref: '#/components/schemas/Group' + description: The event payload. + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + context: + $ref: '#/components/schemas/EventContextDto' + object: + type: string + description: Distinguishes the Event object. + const: event + required: + - id + - event + - data + - created_at + - object - type: object properties: id: @@ -23354,6 +25258,16 @@ components: The ID of the user who accepted the invitation, once accepted. example: null + role_slug: + type: + - string + - 'null' + description: >- + Slug of the role the invitee will be assigned on + acceptance. Reflects the current role on the invitee's + organization membership. null when the invitation has no + associated organization. + example: admin created_at: format: date-time type: string @@ -23375,6 +25289,7 @@ components: - organization_id - inviter_user_id - accepted_user_id + - role_slug - created_at - updated_at description: The event payload. @@ -23475,6 +25390,16 @@ components: The ID of the user who accepted the invitation, once accepted. example: null + role_slug: + type: + - string + - 'null' + description: >- + Slug of the role the invitee will be assigned on + acceptance. Reflects the current role on the invitee's + organization membership. null when the invitation has no + associated organization. + example: admin created_at: format: date-time type: string @@ -23496,6 +25421,7 @@ components: - organization_id - inviter_user_id - accepted_user_id + - role_slug - created_at - updated_at description: The event payload. @@ -23596,6 +25522,16 @@ components: The ID of the user who accepted the invitation, once accepted. example: null + role_slug: + type: + - string + - 'null' + description: >- + Slug of the role the invitee will be assigned on + acceptance. Reflects the current role on the invitee's + organization membership. null when the invitation has no + associated organization. + example: admin created_at: format: date-time type: string @@ -23617,6 +25553,7 @@ components: - organization_id - inviter_user_id - accepted_user_id + - role_slug - created_at - updated_at description: The event payload. @@ -23717,6 +25654,16 @@ components: The ID of the user who accepted the invitation, once accepted. example: null + role_slug: + type: + - string + - 'null' + description: >- + Slug of the role the invitee will be assigned on + acceptance. Reflects the current role on the invitee's + organization membership. null when the invitation has no + associated organization. + example: admin created_at: format: date-time type: string @@ -23738,6 +25685,7 @@ components: - organization_id - inviter_user_id - accepted_user_id + - role_slug - created_at - updated_at description: The event payload. @@ -23925,7 +25873,7 @@ components: description: >- Object containing [metadata](/authkit/metadata) key/value pairs associated with the Organization. - example: &ref_11 + example: &ref_20 tier: diamond propertyNames: maxLength: 40 @@ -24075,7 +26023,7 @@ components: description: >- Object containing [metadata](/authkit/metadata) key/value pairs associated with the Organization. - example: *ref_11 + example: *ref_20 propertyNames: maxLength: 40 maxProperties: 50 @@ -24902,7 +26850,7 @@ components: items: type: string description: The permissions granted by the role. - example: &ref_12 + example: &ref_21 - users:read - users:write created_at: @@ -24986,7 +26934,7 @@ components: items: type: string description: The permissions granted by the role. - example: *ref_12 + example: *ref_21 created_at: format: date-time type: string @@ -25068,7 +27016,7 @@ components: items: type: string description: The permissions granted by the role. - example: *ref_12 + example: *ref_21 created_at: format: date-time type: string @@ -25205,7 +27153,7 @@ components: description: >- Object containing [metadata](/authkit/metadata) key/value pairs associated with the Organization. - example: *ref_11 + example: *ref_20 propertyNames: maxLength: 40 maxProperties: 50 @@ -25636,7 +27584,7 @@ components: items: type: string description: The permissions granted by the role. - example: &ref_13 + example: &ref_22 - users:read - users:write created_at: @@ -25702,7 +27650,7 @@ components: items: type: string description: The permissions granted by the role. - example: *ref_13 + example: *ref_22 created_at: format: date-time type: string @@ -25766,7 +27714,7 @@ components: items: type: string description: The permissions granted by the role. - example: *ref_13 + example: *ref_22 created_at: format: date-time type: string @@ -26729,7 +28677,94 @@ components: - data - created_at - object - example: *ref_14 + - type: object + properties: + id: + type: string + description: Unique identifier for the event. + example: event_01EHZNVPK3SFK441A1RGBFSHRT + event: + type: string + const: waitlist_user.approved + data: + $ref: '#/components/schemas/WaitlistUser' + description: The event payload. + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + context: + $ref: '#/components/schemas/EventContextDto' + object: + type: string + description: Distinguishes the Event object. + const: event + required: + - id + - event + - data + - created_at + - object + - type: object + properties: + id: + type: string + description: Unique identifier for the event. + example: event_01EHZNVPK3SFK441A1RGBFSHRT + event: + type: string + const: waitlist_user.created + data: + $ref: '#/components/schemas/WaitlistUser' + description: The event payload. + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + context: + $ref: '#/components/schemas/EventContextDto' + object: + type: string + description: Distinguishes the Event object. + const: event + required: + - id + - event + - data + - created_at + - object + - type: object + properties: + id: + type: string + description: Unique identifier for the event. + example: event_01EHZNVPK3SFK441A1RGBFSHRT + event: + type: string + const: waitlist_user.denied + data: + $ref: '#/components/schemas/WaitlistUser' + description: The event payload. + created_at: + format: date-time + type: string + description: An ISO 8601 timestamp. + example: '2026-01-15T12:00:00.000Z' + context: + $ref: '#/components/schemas/EventContextDto' + object: + type: string + description: Distinguishes the Event object. + const: event + required: + - id + - event + - data + - created_at + - object + example: *ref_23 description: An event emitted by WorkOS. EventList: type: object @@ -26764,7 +28799,7 @@ components: example: object: list data: - - *ref_14 + - *ref_23 list_metadata: after: event_01EHZNVPK3SFK441A1RGBFSHRT JwtTemplateResponse: @@ -27996,6 +30031,15 @@ components: - 'null' description: The ID of the user who accepted the invitation, once accepted. example: null + role_slug: + type: + - string + - 'null' + description: >- + Slug of the role the invitee will be assigned on acceptance. + Reflects the current role on the invitee's organization membership. + null when the invitation has no associated organization. + example: admin created_at: format: date-time type: string @@ -28026,6 +30070,7 @@ components: - organization_id - inviter_user_id - accepted_user_id + - role_slug - created_at - updated_at - token