diff --git a/.github/workflows/cleanup-pr-preview.yml b/.github/workflows/cleanup-pr-preview.yml new file mode 100644 index 0000000..24a48c0 --- /dev/null +++ b/.github/workflows/cleanup-pr-preview.yml @@ -0,0 +1,45 @@ +name: Cleanup PR preview + +on: + pull_request: + types: [closed] + +permissions: + contents: write + +concurrency: + group: gh-pages-cleanup-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Remove pr-${{ github.event.pull_request.number }} from gh-pages + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + + if ! git ls-remote --exit-code --heads "https://github.com/${{ github.repository }}.git" gh-pages >/dev/null 2>&1; then + echo "gh-pages branch does not exist; nothing to clean up" + exit 0 + fi + + tmp=$(mktemp -d) + git clone --depth=1 --branch gh-pages \ + "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" \ + "$tmp" + + cd "$tmp" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + if [ ! -d "pr-${PR_NUMBER}" ]; then + echo "No preview directory for PR #${PR_NUMBER}" + exit 0 + fi + + git rm -rf "pr-${PR_NUMBER}" + git commit -m "Remove PR #${PR_NUMBER} SDK diff preview" + git push origin gh-pages diff --git a/.github/workflows/validate-sdks.yml b/.github/workflows/validate-sdks.yml index 68bcbc7..e2caa7d 100644 --- a/.github/workflows/validate-sdks.yml +++ b/.github/workflows/validate-sdks.yml @@ -1,7 +1,7 @@ name: Validate SDKs permissions: - contents: read + contents: write issues: write pull-requests: write @@ -161,6 +161,7 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '24' + cache: 'npm' - name: Download diagnostics if: needs.load-matrix.outputs.spec-changed == 'true' @@ -169,26 +170,19 @@ jobs: pattern: oagen-diagnostics-* path: sdk-diagnostics + - name: Install dependencies + if: needs.load-matrix.outputs.spec-changed == 'true' + run: npm ci + - name: Build code diff report if: needs.load-matrix.outputs.spec-changed == 'true' id: diff-report run: | - set -e - : > /tmp/combined.diff - has_content=0 - for lang in dotnet go php python ruby; do - diff_file="sdk-diagnostics/oagen-diagnostics-${lang}/sdk-code.diff" - if [ -s "$diff_file" ]; then - cat "$diff_file" >> /tmp/combined.diff - has_content=1 - fi - done - if [ "$has_content" = "1" ]; then - npx --yes diff2html-cli@5 -i file -s side --su hidden -F /tmp/sdk-diff-report.html /tmp/combined.diff - echo "report-exists=true" >> "$GITHUB_OUTPUT" - else - echo "report-exists=false" >> "$GITHUB_OUTPUT" - fi + languages="$(jq -r 'map(.language) | join(",")' .github/sdk-matrix.json)" + node scripts/build-sdk-diff-report.mjs \ + --artifacts-root sdk-diagnostics \ + --languages "$languages" \ + --output /tmp/sdk-diff-report.html - name: Upload code diff report if: steps.diff-report.outputs.report-exists == 'true' @@ -198,6 +192,49 @@ jobs: path: /tmp/sdk-diff-report.html retention-days: 14 + - name: Publish report to GitHub Pages + id: publish-pages + if: >- + steps.diff-report.outputs.report-exists == 'true' + && github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + worktree=$(mktemp -d) + if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then + git fetch --depth=1 origin gh-pages + git worktree add "$worktree" origin/gh-pages + (cd "$worktree" && git checkout -B gh-pages) + else + git worktree add --orphan -b gh-pages "$worktree" + : > "$worktree/.nojekyll" + fi + + target_dir="$worktree/pr-${PR_NUMBER}" + mkdir -p "$target_dir" + cp /tmp/sdk-diff-report.html "$target_dir/sdk-diff-report.html" + + ( + cd "$worktree" + git add -A + if git diff --cached --quiet; then + echo "No changes to publish" + else + git commit -m "Update PR #${PR_NUMBER} SDK diff preview" + git push origin gh-pages + fi + ) + + owner="${REPO%%/*}" + name="${REPO#*/}" + echo "pages-url=https://${owner}.github.io/${name}/pr-${PR_NUMBER}/sdk-diff-report.html" >> "$GITHUB_OUTPUT" + - name: Render PR comment if: needs.load-matrix.outputs.spec-changed == 'true' run: | @@ -207,6 +244,7 @@ jobs: --run-id "${{ github.run_id }}" \ --repo "${{ github.repository }}" \ --code-diff-available "${{ steps.diff-report.outputs.report-exists }}" \ + --pages-url "${{ steps.publish-pages.outputs.pages-url }}" \ --output /tmp/sdk-compat-comment.md - name: Upsert PR comment diff --git a/.last-synced-sha b/.last-synced-sha index f72eb0e..33f7688 100644 --- a/.last-synced-sha +++ b/.last-synced-sha @@ -1 +1 @@ -92db0495807c86fbbc4d45bd266a6c1f5bcbb59c +ff939ff075453287993e1e6182f1d6f23c67ab80 diff --git a/oagen.config.ts b/oagen.config.ts index bae2838..925d8a0 100644 --- a/oagen.config.ts +++ b/oagen.config.ts @@ -1,4 +1,4 @@ -import type { OagenConfig, OperationHint } from '@workos/oagen'; +import type { OagenConfig, OpenApiDocument, OperationHint } from '@workos/oagen'; import { toCamelCase } from '@workos/oagen'; import { workosEmittersPlugin } from '@workos/oagen-emitters'; @@ -354,6 +354,81 @@ const mountRules: Record = { UserManagementMultiFactorAuthentication: 'MultiFactorAuth', }; +// --------------------------------------------------------------------------- +// Pre-IR spec overlay -- patch around upstream spec quirks that would otherwise +// emit breaking SDK changes. See docs/breaking-change-playbook.md and the +// oagen `transformSpec` docs for usage. Reach for this only when the upstream +// fix can't land in time AND the change is genuinely additive. +// --------------------------------------------------------------------------- +function transformSpec(spec: OpenApiDocument): OpenApiDocument { + const components = (spec as { components?: { schemas?: Record> } }).components; + const schemas = components?.schemas; + const paths = (spec as { paths?: Record> }).paths; + if (!schemas || !paths) return spec; + + // -- Fork: UserlandUserOrganizationMembershipBase{,List} -------------------- + // Upstream forked the existing `…BaseList` into `…BaseWithUserList` to add + // a `user` field on the inline list-item shape. That fork renames the + // generated list-data type in dotnet/go/ruby, breaking compat. Re-point the + // forked $refs at the original list and merge the new `user` field + // additively into the original's inline item shape. + const forkedListRef = '#/components/schemas/UserlandUserOrganizationMembershipBaseWithUserList'; + const originalListRef = '#/components/schemas/UserlandUserOrganizationMembershipBaseList'; + for (const pathItem of Object.values(paths)) { + for (const op of Object.values(pathItem)) { + const responses = (op as { responses?: Record }> }) + .responses; + const schema = responses?.['200']?.content?.['application/json']?.schema; + if (schema?.$ref === forkedListRef) { + schema.$ref = originalListRef; + } + } + } + const baseList = schemas['UserlandUserOrganizationMembershipBaseList']; + const itemProps = ( + baseList as + | { + properties?: { + data?: { items?: { properties?: Record; required?: string[] } }; + }; + } + | undefined + )?.properties?.data?.items; + if (itemProps?.properties && !itemProps.properties.user) { + itemProps.properties.user = { + $ref: '#/components/schemas/UserlandUser', + description: 'The user that belongs to the organization through this membership.', + } as unknown as Record; + if (itemProps.required && !itemProps.required.includes('user')) { + itemProps.required.push('user'); + } + } + delete schemas['UserlandUserOrganizationMembershipBaseWithUser']; + delete schemas['UserlandUserOrganizationMembershipBaseWithUserList']; + + // -- Rename: JwtTemplate -> JwtTemplateResponse ----------------------------- + // Upstream renamed the response schema. Existing SDKs already expose the + // type as `JwtTemplateResponse`/`JWTTemplateResponse`; preserve that name. + if (schemas['JwtTemplate'] && !schemas['JwtTemplateResponse']) { + schemas['JwtTemplateResponse'] = schemas['JwtTemplate']; + delete schemas['JwtTemplate']; + const oldRef = '#/components/schemas/JwtTemplate'; + const newRef = '#/components/schemas/JwtTemplateResponse'; + for (const pathItem of Object.values(paths)) { + for (const op of Object.values(pathItem)) { + const responses = (op as { responses?: Record }> }) + .responses; + for (const response of Object.values(responses ?? {})) { + const schema = response.content?.['application/json']?.schema; + if (schema?.$ref === oldRef) schema.$ref = newRef; + } + } + } + } + + return spec; +} + const config: OagenConfig = { ...workosEmittersPlugin, docUrl: 'https://workos.com/docs', @@ -377,5 +452,13 @@ const config: OagenConfig = { }, operationHints, mountRules, + modelHints: { + // `UserlandUser` (→ `User`) is referenced from both UserManagement and + // Authorization paths; pin it to UserManagement so hand-written imports + // in `workos-python` (e.g. `from workos.user_management.models import User`) + // keep resolving. + User: 'UserManagement', + }, + transformSpec, }; export default config; diff --git a/package-lock.json b/package-lock.json index 3b905c4..887590c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,15 @@ "name": "workos-openapi-spec", "version": "0.0.1", "dependencies": { - "@workos/oagen": "^0.13.0", - "@workos/oagen-emitters": "^0.7.2", + "@workos/oagen": "^0.16.0", + "@workos/oagen-emitters": "^0.7.5", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/node": "^25.6.0", + "diff2html": "^3.4.56", "husky": "^9.1.7", "tsx": "^4.21.0", "typescript": "^6.0.3" @@ -431,6 +432,19 @@ "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", "deprecated": "Please update to a newer version." }, + "node_modules/@profoundlogic/hogan": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@profoundlogic/hogan/-/hogan-3.0.4.tgz", + "integrity": "sha512-pmNVGuooS30Mm7YbZd5T7E5zYVO6D5Ct91sn4T39mUvMUc3sCGridcnhAufL1/Bz2QzAtzEn0agNrdk3+5yWzw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "nopt": "1.0.10" + }, + "bin": { + "hulk": "bin/hulk" + } + }, "node_modules/@redocly/ajv": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.3.tgz", @@ -518,9 +532,9 @@ } }, "node_modules/@workos/oagen": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@workos/oagen/-/oagen-0.13.0.tgz", - "integrity": "sha512-Guv6yJylmi21E1udO1cB9PwaB6iP/VhyDMprFqYIlU4XHcrWZe3sCSnXdi/FgX3+LP7SwTxdZ8c37t00eMRl/g==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@workos/oagen/-/oagen-0.16.0.tgz", + "integrity": "sha512-0rUFFXcrEUSnQPejCcnetpVc9eAbcjcT5K5gDbN6W2MSRTGMACo8ZkBaH9sWxvuFRu33JTWcjqifo0h5xpIXTg==", "license": "MIT", "dependencies": { "@redocly/openapi-core": "^2.25.1", @@ -547,257 +561,17 @@ } }, "node_modules/@workos/oagen-emitters": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@workos/oagen-emitters/-/oagen-emitters-0.7.2.tgz", - "integrity": "sha512-cTIZlizdgpLWJKjtwNnZ3S4/pcq+b85Ek5AEzCTO3VuiUXN/9GoJyOop+R062sv9sArDSDa7MPgnUsvGIKwTGA==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@workos/oagen-emitters/-/oagen-emitters-0.7.5.tgz", + "integrity": "sha512-2GIOaurAoXtXMU4rcqMq6D/31zVfMcPvu59VuHl+Qjz888YdLjsoShzXflQuHB9EYcup9x8uBFviTVyF8Pj8Ig==", "license": "MIT", "dependencies": { - "@workos/oagen": "^0.12.0" + "@workos/oagen": "^0.16.0" }, "engines": { "node": ">=24.10.0" } }, - "node_modules/@workos/oagen-emitters/node_modules/@workos/oagen": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@workos/oagen/-/oagen-0.12.0.tgz", - "integrity": "sha512-bfXANikWQQ/vJmbM1O7t+56NGI625vnObyaB24Npkh/JKRNc9073WwL5Wq2vR6m+ORinqTJKB8kOYU6MIhApqQ==", - "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", @@ -1009,6 +783,13 @@ } } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -1159,6 +940,33 @@ "validate.io-integer-array": "^1.0.0" } }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff2html": { + "version": "3.4.56", + "resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.56.tgz", + "integrity": "sha512-u9gfn+BlbHcyO7vItCIC4z49LJDUt31tODzOfAuJ5R1E7IdlRL6KjugcB9zOpejD+XiR+dDZbsnHSQ3g6A/u8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@profoundlogic/hogan": "^3.0.4", + "diff": "^8.0.3" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "highlight.js": "11.11.1" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -1307,6 +1115,17 @@ "lodash": "^4.17.15" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/http-reasons": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/http-reasons/-/http-reasons-0.1.0.tgz", @@ -1518,6 +1337,22 @@ "es6-promise": "^3.2.1" } }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/oas-kit-common": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", diff --git a/package.json b/package.json index e26c1fc..96bbe4b 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "prepare": "husky" }, "dependencies": { - "@workos/oagen": "^0.13.0", - "@workos/oagen-emitters": "^0.7.2", + "@workos/oagen": "^0.16.0", + "@workos/oagen-emitters": "^0.7.5", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, @@ -35,6 +35,7 @@ "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/node": "^25.6.0", + "diff2html": "^3.4.56", "husky": "^9.1.7", "tsx": "^4.21.0", "typescript": "^6.0.3" diff --git a/scripts/build-sdk-diff-report.mjs b/scripts/build-sdk-diff-report.mjs new file mode 100644 index 0000000..bbf1d01 --- /dev/null +++ b/scripts/build-sdk-diff-report.mjs @@ -0,0 +1,273 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parse, html } from 'diff2html'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const TEST_PATTERNS = { + dotnet: [/(^|\/)Tests?\//, /Tests?\.cs$/i, /\.Tests?\.csproj$/i], + go: [/_test\.go$/], + php: [/(^|\/)tests?\//i], + python: [/(^|\/)tests?\//, /(^|\/)test_[^/]+\.py$/, /[^/]+_test\.py$/, /(^|\/)conftest\.py$/], + ruby: [/(^|\/)spec\//, /(^|\/)test\//, /_spec\.rb$/, /_test\.rb$/], +}; + +function parseArgs(argv) { + const args = { artifactsRoot: '', output: '', languages: [] }; + for (let i = 2; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === '--artifacts-root') args.artifactsRoot = argv[++i] ?? ''; + else if (arg === '--output') args.output = argv[++i] ?? ''; + else if (arg === '--languages') args.languages = (argv[++i] ?? '').split(',').filter(Boolean); + else throw new Error(`Unknown option: ${arg}`); + } + if (!args.artifactsRoot || !args.output || args.languages.length === 0) { + throw new Error('Usage: build-sdk-diff-report.mjs --artifacts-root --output --languages '); + } + return args; +} + +function stripLangPrefix(language, filePath) { + const prefix = `${language}/`; + return filePath.startsWith(prefix) ? filePath.slice(prefix.length) : filePath; +} + +function categorizeFile(language, filePath) { + const stripped = stripLangPrefix(language, filePath); + if (stripped.endsWith('.oagen-manifest.json')) return 'manifest'; + const patterns = TEST_PATTERNS[language] ?? []; + if (patterns.some((p) => p.test(stripped))) return 'test'; + return 'code'; +} + +function effectiveName(file) { + if (file.newName && file.newName !== '/dev/null') return file.newName; + return file.oldName ?? ''; +} + +function renderLanguageBody(language, diffText) { + const trimmed = diffText.trim(); + if (!trimmed) { + return { fileCount: 0, counts: { code: 0, test: 0, manifest: 0 }, body: '

No changes for this language.

' }; + } + + const files = parse(trimmed); + if (files.length === 0) { + return { fileCount: 0, counts: { code: 0, test: 0, manifest: 0 }, body: '

No changes for this language.

' }; + } + + const counts = { code: 0, test: 0, manifest: 0 }; + const blocks = []; + + for (const file of files) { + const filePath = effectiveName(file); + const category = categorizeFile(language, filePath); + counts[category] += 1; + + const fileHtml = html([file], { + outputFormat: 'side-by-side', + drawFileList: false, + matching: 'lines', + }); + + const anchorId = filePath; + const header = `
${escapeHtml(filePath)}
`; + blocks.push( + `
${header}${fileHtml}
`, + ); + } + + return { fileCount: files.length, counts, body: blocks.join('\n') }; +} + +function escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function readDiff(artifactsRoot, language) { + const diffPath = path.join(artifactsRoot, `oagen-diagnostics-${language}`, 'sdk-code.diff'); + if (!fs.existsSync(diffPath)) return ''; + return fs.readFileSync(diffPath, 'utf8'); +} + +function readDiff2HtmlCss() { + return fs.readFileSync( + path.join(__dirname, '..', 'node_modules', 'diff2html', 'bundles', 'css', 'diff2html.min.css'), + 'utf8', + ); +} + +function buildHtml(languageReports) { + const css = readDiff2HtmlCss(); + const sortedLanguages = [...languageReports].sort((a, b) => a.language.localeCompare(b.language)); + + const tabs = sortedLanguages + .map((entry, idx) => { + const totals = `${entry.fileCount} file${entry.fileCount === 1 ? '' : 's'}`; + return `${escapeHtml(entry.language)} ${totals}`; + }) + .join('\n'); + + const panels = sortedLanguages + .map((entry, idx) => { + const summary = `

code: ${entry.counts.code} · tests: ${entry.counts.test} · manifest: ${entry.counts.manifest}

`; + const emptyFiltered = '

All files in this language are hidden by the current filters.

'; + return `
${summary}${entry.body}${emptyFiltered}
`; + }) + .join('\n'); + + return ` + + + +WorkOS SDK code diff report + + + + +
+

WorkOS SDK code diff report

+
+
+ Show: + + + +
+
+
+ +
+${panels} +
+ + + +`; +} + +function main() { + const args = parseArgs(process.argv); + const reports = []; + + for (const language of args.languages) { + const diff = readDiff(args.artifactsRoot, language); + const rendered = renderLanguageBody(language, diff); + reports.push({ language, ...rendered }); + } + + const html = buildHtml(reports); + fs.writeFileSync(args.output, html, 'utf8'); + + const totals = reports.reduce( + (acc, r) => { + acc.files += r.fileCount; + return acc; + }, + { files: 0 }, + ); + + const reportExists = totals.files > 0; + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, `report-exists=${reportExists}\n`); + } + console.log(`Wrote ${args.output} (${totals.files} files across ${reports.length} languages)`); +} + +main(); diff --git a/scripts/sdk-compat-pr-comment.mjs b/scripts/sdk-compat-pr-comment.mjs index 0301cc1..8052be3 100644 --- a/scripts/sdk-compat-pr-comment.mjs +++ b/scripts/sdk-compat-pr-comment.mjs @@ -11,6 +11,7 @@ function parseArgs(argv) { runId: '', repo: '', codeDiffAvailable: false, + pagesUrl: '', }; for (let i = 2; i < argv.length; i += 1) { const arg = argv[i]; @@ -26,13 +27,15 @@ function parseArgs(argv) { args.repo = argv[++i] ?? ''; } else if (arg === '--code-diff-available') { args.codeDiffAvailable = (argv[++i] ?? '') === 'true'; + } else if (arg === '--pages-url') { + args.pagesUrl = argv[++i] ?? ''; } else { throw new Error(`Unknown option: ${arg}`); } } if (!args.artifactsRoot || !args.output) { - throw new Error('Usage: sdk-compat-pr-comment.mjs --artifacts-root --output [--build-result ] [--run-id ] [--repo ] [--code-diff-available true|false]'); + throw new Error('Usage: sdk-compat-pr-comment.mjs --artifacts-root --output [--build-result ] [--run-id ] [--repo ] [--code-diff-available true|false] [--pages-url ]'); } return args; @@ -1058,7 +1061,7 @@ function renderDomainSummary(lines, rows, languages, deriveDomain) { // --------------------------------------------------------------------------- function renderMarkdown(languageData, options) { - const { buildResult, runId, repo, codeDiffAvailable } = options; + const { buildResult, runId, repo, codeDiffAvailable, pagesUrl } = options; const rollup = buildRollup(languageData); const lines = []; @@ -1071,7 +1074,10 @@ function renderMarkdown(languageData, options) { lines.push(''); } - if (codeDiffAvailable && runId && repo) { + if (codeDiffAvailable && pagesUrl) { + lines.push(`📄 [View full code diff](${pagesUrl})`); + lines.push(''); + } else if (codeDiffAvailable && runId && repo) { lines.push( `📄 Full code diff: [download report](https://github.com/${repo}/actions/runs/${runId}#artifacts) — open \`sdk-diff-report.html\` from the \`sdk-code-diff-report\` artifact.`, ); @@ -1139,6 +1145,7 @@ function main() { runId: args.runId, repo: args.repo, codeDiffAvailable: args.codeDiffAvailable, + pagesUrl: args.pagesUrl, }); fs.writeFileSync(args.output, markdown, 'utf8'); } diff --git a/spec/open-api-spec.yaml b/spec/open-api-spec.yaml index 13d91c5..09e09ca 100644 --- a/spec/open-api-spec.yaml +++ b/spec/open-api-spec.yaml @@ -3378,7 +3378,7 @@ paths: application/json: schema: $ref: >- - #/components/schemas/UserlandUserOrganizationMembershipBaseList + #/components/schemas/UserlandUserOrganizationMembershipBaseWithUserList '400': description: Bad Request content: @@ -4604,7 +4604,7 @@ paths: application/json: schema: $ref: >- - #/components/schemas/UserlandUserOrganizationMembershipBaseList + #/components/schemas/UserlandUserOrganizationMembershipBaseWithUserList '400': description: Bad Request content: @@ -8295,7 +8295,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiKeyList' + $ref: '#/components/schemas/OrganizationApiKeyList' '404': description: Not Found content: @@ -8335,7 +8335,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiKeyWithValue' + $ref: '#/components/schemas/OrganizationApiKeyWithValue' '404': description: Not Found content: @@ -12208,6 +12208,33 @@ paths: tags: - user-management.invitations /user_management/jwt_template: + get: + operationId: JwtTemplatesController_getJwtTemplate + summary: Get JWT template + description: Get the JWT template for the current environment. + parameters: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/JwtTemplate' + '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.jwt-template put: operationId: JwtTemplatesController_updateJwtTemplate summary: Update JWT template @@ -12225,7 +12252,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/JwtTemplateResponse' + $ref: '#/components/schemas/JwtTemplate' '422': description: Unprocessable Entity content: @@ -12689,6 +12716,11 @@ paths: description: >- The primary role assigned to the user within the organization. + user: + $ref: '#/components/schemas/UserlandUser' + description: >- + The user that belongs to the organization through this + membership. required: - object - id @@ -12699,6 +12731,7 @@ paths: - created_at - updated_at - role + - user x-inline-with-overrides: true '400': description: Bad Request @@ -13045,7 +13078,7 @@ paths: user_id: type: string description: The ID of the user. - example: user_01EHQTV6MWP9P1F4ZXGXMC8ABB + example: user_01E4ZCR3C56J083X43JQXF3JK5 organization_id: type: string description: The ID of the organization which the user belongs to. @@ -13098,6 +13131,11 @@ paths: description: >- The primary role assigned to the user within the organization. + user: + $ref: '#/components/schemas/UserlandUser' + description: >- + The user that belongs to the organization through this + membership. required: - object - id @@ -13108,6 +13146,7 @@ paths: - created_at - updated_at - role + - user x-inline-with-overrides: true '400': description: Bad Request @@ -15370,6 +15409,181 @@ paths: - message tags: - user-management.users + /user_management/users/{userId}/api_keys: + get: + operationId: UserApiKeysController_list + summary: List API keys for a user + description: Get a list of API keys owned by a specific user. + parameters: + - name: userId + required: true + in: path + description: Unique identifier of the user. + schema: + type: string + example: user_01EHZNVPK3SFK441A1RGBFSHRT + - 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. + schema: + example: obj_1234567890 + 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. + schema: + example: obj_1234567890 + 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. + schema: + default: desc + example: desc + enum: + - normal + - desc + - asc + type: string + - name: organization_id + required: false + in: query + description: >- + The ID of the organization to filter user API keys by. When + provided, only API keys created against that organization membership + are returned. + schema: + example: org_01EHZNVPK3SFK441A1RGBFSHRT + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserApiKeyList' + '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: + - api_keys + x-feature-flag: user-api-keys + post: + operationId: UserApiKeysController_create + summary: Create an API key for a user + description: >- + Create a new API key owned by a user. The user must have an active + membership in the specified organization. + parameters: + - name: userId + required: true + in: path + description: Unique identifier of the user. + schema: + type: string + example: user_01EHZNVPK3SFK441A1RGBFSHRT + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserApiKeyDto' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/UserApiKeyWithValue' + '400': + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A human-readable description of the error. + example: Validation failed. + errors: + type: array + items: + type: object + properties: + code: + type: string + description: The validation error code. + example: required + field: + type: string + description: The field that failed validation. + example: event.action + required: + - code + - field + description: The list of validation errors. + required: + - message + - errors + '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: + - api_keys + x-feature-flag: user-api-keys /user_management/users/{userId}/feature-flags: get: operationId: UserlandUserFeatureFlagsController_list @@ -17469,7 +17683,9 @@ components: content: type: string description: The JWT template content as a Liquid template string. - example: '{"iss": "{{environment.id}}", "sub": "{{user.id}}"}' + example: >- + {"urn:myapp:full_name": "{{user.first_name}} {{user.last_name}}", + "urn:myapp:email": "{{user.email}}"} required: - content CreateOrganizationDomainDto: @@ -18120,6 +18336,32 @@ components: x-exclude-from-lint: true required: - role_slug + CreateUserApiKeyDto: + type: object + properties: + name: + type: string + description: A descriptive name for the API key. + example: Production API Key + organization_id: + type: string + description: >- + The ID of the organization the user API key is associated with. The + user must have an active membership in this organization. + example: org_01EHZNVPK3SFK441A1RGBFSHRT + permissions: + description: >- + The permission slugs to assign to the API key. Each permission must + be enabled for user API keys. + example: + - posts:read + - posts:write + type: array + items: + type: string + required: + - name + - organization_id CreateUserlandUserDto: allOf: - type: object @@ -18615,21 +18857,48 @@ components: description: Unique identifier of the API Key. example: api_key_01EHZNVPK3SFK441A1RGBFSHRT owner: - type: object - properties: - type: - type: string - description: The type of the API Key owner. - example: organization - const: organization - id: - type: string - description: Unique identifier of the API Key owner. - example: org_01EHZNVPK3SFK441A1RGBFSHRT - required: - - type - - id + discriminator: + propertyName: type + oneOf: + - type: object + properties: + type: + type: string + description: The type of the API Key owner. + example: organization + const: organization + id: + type: string + description: Unique identifier of the API Key owner. + example: org_01EHZNVPK3SFK441A1RGBFSHRT + required: + - type + - id + - type: object + properties: + type: + type: string + description: The type of the API Key owner. + example: user + const: user + id: + type: string + description: Unique identifier of the API Key owner. + example: user_01EHZNVPK3SFK441A1RGBFSHRT + organization_id: + type: string + description: >- + Unique identifier of the organization the API Key can + access. + example: org_01EHZNVPK3SFK441A1RGBFSHRT + required: + - type + - id + - organization_id description: The entity that owns the API Key. + example: + type: organization + id: org_01EHZNVPK3SFK441A1RGBFSHRT name: type: string description: A descriptive name for the API Key. @@ -18673,7 +18942,6 @@ components: - permissions - created_at - updated_at - description: The API Key object if the value is valid, or `null` if invalid. ApiKeyValidationResponse: type: object properties: @@ -19772,85 +20040,182 @@ components: required: - object - data - UserlandUserOrganizationMembershipBaseList: + UserlandUser: type: object properties: object: type: string - description: Indicates this is a list response. - const: list - data: + description: Distinguishes the user object. + const: user + id: + type: string + description: The unique ID of the user. + example: user_01E4ZCR3C56J083X43JQXF3JK5 + 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 + profile_picture_url: + type: + - string + - 'null' + description: A URL reference to an image representing the user. + example: https://workoscdn.com/images/v1/123abc + email: + type: string + description: The email address of the user. + example: marcelina.davis@example.com + email_verified: + type: boolean + description: Whether the user's email has been verified. + example: true + external_id: + type: + - string + - 'null' + description: The external ID of the user. + example: f1ffa2b2-c20b-4d39-be5c-212726e11222 + metadata: + type: object + additionalProperties: + type: string + maxLength: 600 + description: Object containing metadata key/value pairs associated with the user. + example: + timezone: America/New_York + propertyNames: + maxLength: 40 + maxProperties: 50 + last_sign_in_at: + format: date-time + type: + - string + - 'null' + description: The timestamp when the user last signed in. + example: '2025-06-25T19:07:33.155Z' + locale: + type: + - string + - 'null' + description: The user's preferred locale. + example: en-US + 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 + - first_name + - last_name + - profile_picture_url + - email + - email_verified + - external_id + - last_sign_in_at + - created_at + - updated_at + description: The user object. + UserlandUserOrganizationMembershipBaseWithUser: + type: object + properties: + object: + type: string + description: Distinguishes the organization membership object. + const: organization_membership + id: + type: string + description: The unique ID of the organization membership. + example: om_01HXYZ123456789ABCDEFGHIJ + user_id: + type: string + description: The ID of the user. + example: user_01E4ZCR3C56J083X43JQXF3JK5 + organization_id: + type: string + description: The ID of the organization which the user belongs to. + example: org_01EHZNVPK3SFK441A1RGBFSHRT + status: + type: string + enum: + - active + - inactive + - pending + description: >- + The status of the organization membership. One of `active`, + `inactive`, or `pending`. + example: active + directory_managed: + type: boolean + description: >- + Whether this organization membership is managed by a directory sync + connection. + example: false + organization_name: + type: string + description: The name of the organization which the user belongs to. + example: Acme Corp + custom_attributes: + type: object + additionalProperties: {} + description: >- + An object containing IdP-sourced attributes from the linked + [Directory User](/reference/directory-sync/directory-user) or [SSO + Profile](/reference/sso/profile). Directory User attributes take + precedence when both are linked. + example: + department: Engineering + title: Developer Experience Engineer + location: Brooklyn + 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' + user: + $ref: '#/components/schemas/UserlandUser' + description: The user that belongs to the organization through this membership. + required: + - object + - id + - user_id + - organization_id + - status + - directory_managed + - created_at + - updated_at + - user + UserlandUserOrganizationMembershipBaseWithUserList: + type: object + properties: + object: + type: string + description: Indicates this is a list response. + const: list + data: type: array items: - type: object - properties: - object: - type: string - description: Distinguishes the organization membership object. - const: organization_membership - id: - type: string - description: The unique ID of the organization membership. - example: om_01HXYZ123456789ABCDEFGHIJ - user_id: - type: string - description: The ID of the user. - example: user_01EHQTV6MWP9P1F4ZXGXMC8ABB - organization_id: - type: string - description: The ID of the organization which the user belongs to. - example: org_01EHZNVPK3SFK441A1RGBFSHRT - status: - type: string - enum: - - active - - inactive - - pending - description: >- - The status of the organization membership. One of `active`, - `inactive`, or `pending`. - example: active - directory_managed: - type: boolean - description: >- - Whether this organization membership is managed by a directory - sync connection. - example: false - organization_name: - type: string - description: The name of the organization which the user belongs to. - example: Acme Corp - custom_attributes: - type: object - additionalProperties: {} - description: >- - An object containing IdP-sourced attributes from the linked - [Directory User](/reference/directory-sync/directory-user) or - [SSO Profile](/reference/sso/profile). Directory User - attributes take precedence when both are linked. - example: - department: Engineering - title: Developer Experience Engineer - location: Brooklyn - 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 - - user_id - - organization_id - - status - - directory_managed - - created_at - - updated_at + $ref: >- + #/components/schemas/UserlandUserOrganizationMembershipBaseWithUser description: The list of records for the current page. list_metadata: type: object @@ -20397,6 +20762,12 @@ components: - 'null' description: The last name of the user. example: Davis + name: + type: + - string + - 'null' + description: The full name of the user. + example: Marcelina Davis emails: type: array items: @@ -20476,8 +20847,14 @@ components: items: $ref: '#/components/schemas/DirectoryGroup' description: >- - The directory groups the user belongs to. Use the List Directory - Groups endpoint with a user filter instead. + The directory groups the user belongs to. Deprecated: starting May + 1, 2026, this field returns an empty array by default for newly + created teams. Existing teams currently depending on this field + should migrate to the new access pattern for better throughput + performance — the field is unbounded by user, so users with many + group memberships produce large, slow response payloads. Use the + List Directory Groups endpoint with a `user` filter to fetch a + user's group memberships. deprecated: true required: - object @@ -20532,6 +20909,113 @@ components: - data - list_metadata - list_metadata + UserlandUserOrganizationMembershipBaseList: + type: object + properties: + object: + type: string + description: Indicates this is a list response. + const: list + data: + type: array + items: + type: object + properties: + object: + type: string + description: Distinguishes the organization membership object. + const: organization_membership + id: + type: string + description: The unique ID of the organization membership. + example: om_01HXYZ123456789ABCDEFGHIJ + user_id: + type: string + description: The ID of the user. + example: user_01E4ZCR3C56J083X43JQXF3JK5 + organization_id: + type: string + description: The ID of the organization which the user belongs to. + example: org_01EHZNVPK3SFK441A1RGBFSHRT + status: + type: string + enum: + - active + - inactive + - pending + description: >- + The status of the organization membership. One of `active`, + `inactive`, or `pending`. + example: active + directory_managed: + type: boolean + description: >- + Whether this organization membership is managed by a directory + sync connection. + example: false + organization_name: + type: string + description: The name of the organization which the user belongs to. + example: Acme Corp + custom_attributes: + type: object + additionalProperties: {} + description: >- + An object containing IdP-sourced attributes from the linked + [Directory User](/reference/directory-sync/directory-user) or + [SSO Profile](/reference/sso/profile). Directory User + attributes take precedence when both are linked. + example: + department: Engineering + title: Developer Experience Engineer + location: Brooklyn + 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 + - user_id + - organization_id + - status + - directory_managed + - created_at + - updated_at + 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: om_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: om_01HXYZ987654321KJIHGFEDCBA + required: + - before + - after + description: Pagination cursors for navigating between pages of results. + required: + - object + - data + - list_metadata Group: type: object properties: @@ -20627,6 +21111,7 @@ components: enum: - api - dashboard + - admin_portal - system description: The source of the actor that performed the action. name: @@ -20725,6 +21210,12 @@ components: - 'null' description: The last name of the user. example: Davis + name: + type: + - string + - 'null' + description: The full name of the user. + example: Marcelina Davis emails: type: array items: @@ -20775,109 +21266,20 @@ components: deprecated: true custom_attributes: type: object - additionalProperties: {} - description: >- - An object containing the custom attribute mapping for the Directory - Provider. - example: &ref_18 - department: Engineering - job_title: Software Engineer - role: - $ref: '#/components/schemas/SlimRole' - roles: - type: array - items: - $ref: '#/components/schemas/SlimRole' - description: All roles assigned to the user. - 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 - - directory_id - - organization_id - - idp_id - - email - - state - - raw_attributes - - custom_attributes - - created_at - - updated_at - UserlandUser: - type: object - properties: - object: - type: string - description: Distinguishes the user object. - const: user - id: - type: string - description: The unique ID of the user. - example: user_01E4ZCR3C56J083X43JQXF3JK5 - 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 - profile_picture_url: - type: - - string - - 'null' - description: A URL reference to an image representing the user. - example: https://workoscdn.com/images/v1/123abc - email: - type: string - description: The email address of the user. - example: marcelina.davis@example.com - email_verified: - type: boolean - description: Whether the user's email has been verified. - example: true - external_id: - type: - - string - - 'null' - description: The external ID of the user. - example: f1ffa2b2-c20b-4d39-be5c-212726e11222 - metadata: - type: object - additionalProperties: - type: string - maxLength: 600 - description: Object containing metadata key/value pairs associated with the user. - example: - timezone: America/New_York - propertyNames: - maxLength: 40 - maxProperties: 50 - last_sign_in_at: - format: date-time - type: - - string - - 'null' - description: The timestamp when the user last signed in. - example: '2025-06-25T19:07:33.155Z' - locale: - type: - - string - - 'null' - description: The user's preferred locale. - example: en-US + additionalProperties: {} + description: >- + An object containing the custom attribute mapping for the Directory + Provider. + example: &ref_18 + department: Engineering + job_title: Software Engineer + role: + $ref: '#/components/schemas/SlimRole' + roles: + type: array + items: + $ref: '#/components/schemas/SlimRole' + description: All roles assigned to the user. created_at: format: date-time type: string @@ -20891,16 +21293,15 @@ components: required: - object - id - - first_name - - last_name - - profile_picture_url + - directory_id + - organization_id + - idp_id - email - - email_verified - - external_id - - last_sign_in_at + - state + - raw_attributes + - custom_attributes - created_at - updated_at - description: The user object. WaitlistUser: type: object properties: @@ -21194,19 +21595,42 @@ components: description: Unique identifier of the API key. example: api_key_01EHWNCE74X7JSDV0X3SZ3KJNY owner: - type: object - properties: - type: - type: string - description: The type of the API key owner. - const: organization - id: - type: string - description: The unique identifier of the API key owner. - example: org_01EHWNCE74X7JSDV0X3SZ3KJNY - required: - - type - - id + oneOf: + - type: object + properties: + type: + type: string + description: The type of the API key owner. + const: organization + id: + type: string + description: The unique identifier of the API key owner. + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + required: + - type + - id + - type: object + properties: + type: + type: string + description: The type of the API key owner. + const: user + id: + type: string + description: >- + The unique identifier of the user who owns the + API key. + example: user_01EHWNCE74X7JSDV0X3SZ3KJNY + organization_id: + type: string + description: >- + The unique identifier of the organization the + API key belongs to. + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + required: + - type + - id + - organization_id description: The owner of the API key. name: type: string @@ -21287,19 +21711,42 @@ components: description: Unique identifier of the API key. example: api_key_01EHWNCE74X7JSDV0X3SZ3KJNY owner: - type: object - properties: - type: - type: string - description: The type of the API key owner. - const: organization - id: - type: string - description: The unique identifier of the API key owner. - example: org_01EHWNCE74X7JSDV0X3SZ3KJNY - required: - - type - - id + oneOf: + - type: object + properties: + type: + type: string + description: The type of the API key owner. + const: organization + id: + type: string + description: The unique identifier of the API key owner. + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + required: + - type + - id + - type: object + properties: + type: + type: string + description: The type of the API key owner. + const: user + id: + type: string + description: >- + The unique identifier of the user who owns the + API key. + example: user_01EHWNCE74X7JSDV0X3SZ3KJNY + organization_id: + type: string + description: >- + The unique identifier of the organization the + API key belongs to. + example: org_01EHWNCE74X7JSDV0X3SZ3KJNY + required: + - type + - id + - organization_id description: The owner of the API key. name: type: string @@ -24044,6 +24491,12 @@ components: - 'null' description: The last name of the user. example: Davis + name: + type: + - string + - 'null' + description: The full name of the user. + example: Marcelina Davis emails: type: array items: @@ -24350,6 +24803,7 @@ components: enum: - api - dashboard + - admin_portal - system description: The source of the actor that performed the action. name: @@ -24509,6 +24963,7 @@ components: enum: - api - dashboard + - admin_portal - system description: The source of the actor that performed the action. name: @@ -24668,6 +25123,7 @@ components: enum: - api - dashboard + - admin_portal - system name: type: @@ -24947,6 +25403,7 @@ components: enum: - api - dashboard + - admin_portal - system description: The source of the actor that performed the action. name: @@ -28124,6 +28581,51 @@ components: - data - created_at - object + - type: object + properties: + id: + type: string + description: Unique identifier for the event. + example: event_01EHZNVPK3SFK441A1RGBFSHRT + event: + type: string + const: vault.byok_key.deleted + data: + type: object + properties: + organization_id: + type: string + description: The unique identifier of the organization. + example: org_01EHT88Z8J8795GZNQ4ZP1J81T + key_provider: + type: string + enum: + - AWS_KMS + - GCP_KMS + - AZURE_KEY_VAULT + description: The external key provider used for BYOK. + example: AWS_KMS + required: + - organization_id + - key_provider + 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: @@ -28802,7 +29304,7 @@ components: - *ref_23 list_metadata: after: event_01EHZNVPK3SFK441A1RGBFSHRT - JwtTemplateResponse: + JwtTemplate: type: object properties: object: @@ -28812,7 +29314,9 @@ components: content: type: string description: The JWT template content as a Liquid template string. - example: '{"iss": "{{environment.id}}", "sub": "{{user.id}}"}' + example: >- + {"urn:myapp:full_name": "{{user.first_name}} {{user.last_name}}", + "urn:myapp:email": "{{user.email}}"} created_at: type: string description: The timestamp when the JWT template was created. @@ -28946,19 +29450,130 @@ components: type: string description: Labels assigned to the Feature Flag for categorizing and filtering. example: - - reports - enabled: - type: boolean - description: >- - Specifies whether the Feature Flag is active for the current - environment. - example: true - default_value: - type: boolean - description: >- - The value returned for users and organizations who don't match any - configured targeting rules. - example: false + - reports + enabled: + type: boolean + description: >- + Specifies whether the Feature Flag is active for the current + environment. + example: true + default_value: + type: boolean + description: >- + The value returned for users and organizations who don't match any + configured targeting rules. + example: false + 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 + - owner + - tags + - enabled + - default_value + - created_at + - updated_at + FlagList: + type: object + properties: + object: + type: string + description: Indicates this is a list response. + const: list + data: + type: array + items: + $ref: '#/components/schemas/Flag' + 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: flag_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: flag_01HXYZ987654321KJIHGFEDCBA + required: + - before + - after + description: Pagination cursors for navigating between pages of results. + required: + - object + - data + - list_metadata + OrganizationApiKey: + type: object + properties: + object: + type: string + description: Distinguishes the API Key object. + const: api_key + id: + type: string + description: Unique identifier of the API Key. + example: api_key_01EHZNVPK3SFK441A1RGBFSHRT + owner: + type: object + properties: + type: + type: string + description: The type of the API Key owner. + example: organization + const: organization + id: + type: string + description: Unique identifier of the API Key owner. + example: org_01EHZNVPK3SFK441A1RGBFSHRT + required: + - type + - id + description: The entity that owns the API Key. + name: + type: string + description: A descriptive name for the API Key. + example: Production API Key + obfuscated_value: + type: string + description: An obfuscated representation of the API Key value. + example: sk_...3456 + last_used_at: + type: + - string + - 'null' + format: date-time + description: Timestamp of when the API Key was last used. + example: null + permissions: + type: array + items: + type: string + description: The permission slugs assigned to the API Key. + example: + - posts:read + - posts:write created_at: format: date-time type: string @@ -28972,55 +29587,14 @@ components: required: - object - id - - slug - - name - - description - owner - - tags - - enabled - - default_value + - name + - obfuscated_value + - last_used_at + - permissions - created_at - updated_at - FlagList: - type: object - properties: - object: - type: string - description: Indicates this is a list response. - const: list - data: - type: array - items: - $ref: '#/components/schemas/Flag' - 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: flag_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: flag_01HXYZ987654321KJIHGFEDCBA - required: - - before - - after - description: Pagination cursors for navigating between pages of results. - required: - - object - - data - - list_metadata - ApiKeyList: + OrganizationApiKeyList: type: object properties: object: @@ -29030,7 +29604,7 @@ components: data: type: array items: - $ref: '#/components/schemas/ApiKey' + $ref: '#/components/schemas/OrganizationApiKey' description: The list of records for the current page. list_metadata: type: object @@ -29059,7 +29633,7 @@ components: - object - data - list_metadata - ApiKeyWithValue: + OrganizationApiKeyWithValue: type: object properties: object: @@ -30089,7 +30663,7 @@ components: user_id: type: string description: The ID of the user. - example: user_01EHQTV6MWP9P1F4ZXGXMC8ABB + example: user_01E4ZCR3C56J083X43JQXF3JK5 organization_id: type: string description: The ID of the organization which the user belongs to. @@ -30139,6 +30713,9 @@ components: role: $ref: '#/components/schemas/SlimRole' description: The primary role assigned to the user within the organization. + user: + $ref: '#/components/schemas/UserlandUser' + description: The user that belongs to the organization through this membership. required: - object - id @@ -30149,6 +30726,201 @@ components: - created_at - updated_at - role + - user + UserApiKey: + type: object + properties: + object: + type: string + description: Distinguishes the API Key object. + const: api_key + id: + type: string + description: Unique identifier of the API Key. + example: api_key_01EHZNVPK3SFK441A1RGBFSHRT + owner: + type: object + properties: + type: + type: string + description: The type of the API Key owner. + example: user + const: user + id: + type: string + description: Unique identifier of the API Key owner. + example: user_01EHZNVPK3SFK441A1RGBFSHRT + organization_id: + type: string + description: Unique identifier of the organization the API Key can access. + example: org_01EHZNVPK3SFK441A1RGBFSHRT + required: + - type + - id + - organization_id + description: The entity that owns the API Key. + name: + type: string + description: A descriptive name for the API Key. + example: Production API Key + obfuscated_value: + type: string + description: An obfuscated representation of the API Key value. + example: sk_...3456 + last_used_at: + type: + - string + - 'null' + format: date-time + description: Timestamp of when the API Key was last used. + example: null + permissions: + type: array + items: + type: string + description: The permission slugs assigned to the API Key. + example: + - posts:read + - posts:write + 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 + - owner + - name + - obfuscated_value + - last_used_at + - permissions + - created_at + - updated_at + UserApiKeyList: + type: object + properties: + object: + type: string + description: Indicates this is a list response. + const: list + data: + type: array + items: + $ref: '#/components/schemas/UserApiKey' + 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: api_key_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: api_key_01HXYZ987654321KJIHGFEDCBA + required: + - before + - after + description: Pagination cursors for navigating between pages of results. + required: + - object + - data + - list_metadata + UserApiKeyWithValue: + type: object + properties: + object: + type: string + description: Distinguishes the API Key object. + const: api_key + id: + type: string + description: Unique identifier of the API Key. + example: api_key_01EHZNVPK3SFK441A1RGBFSHRT + owner: + type: object + properties: + type: + type: string + description: The type of the API Key owner. + example: user + const: user + id: + type: string + description: Unique identifier of the API Key owner. + example: user_01EHZNVPK3SFK441A1RGBFSHRT + organization_id: + type: string + description: Unique identifier of the organization the API Key can access. + example: org_01EHZNVPK3SFK441A1RGBFSHRT + required: + - type + - id + - organization_id + description: The entity that owns the API Key. + name: + type: string + description: A descriptive name for the API Key. + example: Production API Key + obfuscated_value: + type: string + description: An obfuscated representation of the API Key value. + example: sk_...3456 + last_used_at: + type: + - string + - 'null' + format: date-time + description: Timestamp of when the API Key was last used. + example: null + permissions: + type: array + items: + type: string + description: The permission slugs assigned to the API Key. + example: + - posts:read + - posts:write + 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' + value: + type: string + description: The full API Key value. Only returned once at creation time. + example: sk_abcdefghijklmnop123456 + required: + - object + - id + - owner + - name + - obfuscated_value + - last_used_at + - permissions + - created_at + - updated_at + - value UserlandUserList: type: object properties: @@ -30694,6 +31466,12 @@ components: - 'null' description: The user's last name. example: Rundgren + name: + type: + - string + - 'null' + description: The user's full name. + example: Todd Rundgren role: description: >- The role assigned to the user within the organization, if @@ -30742,6 +31520,7 @@ components: - email - first_name - last_name + - name - raw_attributes SsoTokenResponse: type: object