From 63f688e77ef5da56153f760172bbe3b46b6b74a1 Mon Sep 17 00:00:00 2001 From: "workos-sdk-automation[bot]" <255426317+workos-sdk-automation[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 17:55:39 +0000 Subject: [PATCH 1/9] Update OpenAPI spec from workos/workos@ff939ff075453287993e1e6182f1d6f23c67ab80 --- .last-synced-sha | 2 +- spec/open-api-spec.yaml | 1373 ++++++++++++++++++++++++++++++--------- 2 files changed, 1077 insertions(+), 298 deletions(-) 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/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 From b026070e5b2608f3d774a79bd3d5e6036b0354e6 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Fri, 1 May 2026 14:40:46 -0400 Subject: [PATCH 2/9] fix: pin User to UserManagement, unfork membership and JWT schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - modelHints: { User: 'UserManagement' } so workos-python's hand-written `from workos.user_management.models import User` keeps resolving after the upstream spec change moved UserlandUser's first reference into the Authorization service. - transformSpec collapses two breaking renames in the 2026-05-01 spec before IR extraction: rewrites UserlandUserOrganizationMembershipBaseWithUser{,List} back to the existing …Base{,List} (merging the new `user` field additively into the original list-item shape), and renames JwtTemplate back to JwtTemplateResponse. Restores Go/dotnet/Ruby compat without forcing the spec author to revert upstream. - Bump @workos/oagen-emitters to ^0.7.2 to pick up the PHP degenerate- union fromArray fix (workos/oagen-emitters#72), which unbreaks ApiKey::__construct now that owner is a discriminated union. Co-Authored-By: Claude Opus 4.7 (1M context) --- oagen.config.ts | 85 ++++++++++++++++++++++++++++++++++++++++++++++- package-lock.json | 8 ++--- package.json | 2 +- 3 files changed, 89 insertions(+), 6 deletions(-) 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 9479c8f..3b905c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "dependencies": { "@workos/oagen": "^0.13.0", - "@workos/oagen-emitters": "^0.7.1", + "@workos/oagen-emitters": "^0.7.2", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, @@ -547,9 +547,9 @@ } }, "node_modules/@workos/oagen-emitters": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@workos/oagen-emitters/-/oagen-emitters-0.7.1.tgz", - "integrity": "sha512-4KgfgnwpHVaYIxi4RanCFvjNwqCYe2YoW3aOVyEG6m6nJiysZ0v6+E0tmxx6xp8rajB4Blsbqu7Yf9Y7Lipikg==", + "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==", "license": "MIT", "dependencies": { "@workos/oagen": "^0.12.0" diff --git a/package.json b/package.json index 74e271c..e26c1fc 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@workos/oagen": "^0.13.0", - "@workos/oagen-emitters": "^0.7.1", + "@workos/oagen-emitters": "^0.7.2", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, From 7d6ba50f11d5e637e8bdaa798055421bc4c76a9c Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Sat, 2 May 2026 14:11:21 -0400 Subject: [PATCH 3/9] chore: update deps --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b905c4..3b96717 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "workos-openapi-spec", "version": "0.0.1", "dependencies": { - "@workos/oagen": "^0.13.0", + "@workos/oagen": "^0.14.0", "@workos/oagen-emitters": "^0.7.2", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" @@ -518,9 +518,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.14.0", + "resolved": "https://registry.npmjs.org/@workos/oagen/-/oagen-0.14.0.tgz", + "integrity": "sha512-TIdyHIB3tlzkVpr82qJwrYLESc2p+ZsgGUtQ7EyLrxQrSBh0k96bqwvwkfE17Cennx46K6VKHwZsU5i1zNT+Wg==", "license": "MIT", "dependencies": { "@redocly/openapi-core": "^2.25.1", diff --git a/package.json b/package.json index e26c1fc..9269205 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "prepare": "husky" }, "dependencies": { - "@workos/oagen": "^0.13.0", + "@workos/oagen": "^0.14.0", "@workos/oagen-emitters": "^0.7.2", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" From 7ae51b81b582053471b9975da2149663eadb1038 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Sat, 2 May 2026 18:03:28 -0400 Subject: [PATCH 4/9] chore: update deps --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b96717..8489c6c 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.14.0", - "@workos/oagen-emitters": "^0.7.2", + "@workos/oagen": "^0.15.0", + "@workos/oagen-emitters": "^0.7.3", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, @@ -518,9 +518,9 @@ } }, "node_modules/@workos/oagen": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@workos/oagen/-/oagen-0.14.0.tgz", - "integrity": "sha512-TIdyHIB3tlzkVpr82qJwrYLESc2p+ZsgGUtQ7EyLrxQrSBh0k96bqwvwkfE17Cennx46K6VKHwZsU5i1zNT+Wg==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@workos/oagen/-/oagen-0.15.0.tgz", + "integrity": "sha512-U1al58616PCGNBbapY8NZJ5pFenvCZQf4CaPScLakG4edUJAzZX5yOThAs4nDRIMUJJT9dRcLJjeUaH8WN8Y4Q==", "license": "MIT", "dependencies": { "@redocly/openapi-core": "^2.25.1", @@ -547,9 +547,9 @@ } }, "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.3", + "resolved": "https://registry.npmjs.org/@workos/oagen-emitters/-/oagen-emitters-0.7.3.tgz", + "integrity": "sha512-XKCOy7G4aYsrN/d4UZgp+RLV83rhgh05CUk5UzG8DIbzgtfCbkiaoLPn3zEZsVZOSFJfE9CSnNtsQpd0v7AGmA==", "license": "MIT", "dependencies": { "@workos/oagen": "^0.12.0" diff --git a/package.json b/package.json index 9269205..c305f46 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "prepare": "husky" }, "dependencies": { - "@workos/oagen": "^0.14.0", - "@workos/oagen-emitters": "^0.7.2", + "@workos/oagen": "^0.15.0", + "@workos/oagen-emitters": "^0.7.3", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, From 4f5cc8ac862b73bff947d98c59c0940b308ab731 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Sat, 2 May 2026 18:22:00 -0400 Subject: [PATCH 5/9] chore: update deps --- package-lock.json | 250 +--------------------------------------------- package.json | 2 +- 2 files changed, 6 insertions(+), 246 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8489c6c..a7db366 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "dependencies": { "@workos/oagen": "^0.15.0", - "@workos/oagen-emitters": "^0.7.3", + "@workos/oagen-emitters": "^0.7.4", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, @@ -547,257 +547,17 @@ } }, "node_modules/@workos/oagen-emitters": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@workos/oagen-emitters/-/oagen-emitters-0.7.3.tgz", - "integrity": "sha512-XKCOy7G4aYsrN/d4UZgp+RLV83rhgh05CUk5UzG8DIbzgtfCbkiaoLPn3zEZsVZOSFJfE9CSnNtsQpd0v7AGmA==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@workos/oagen-emitters/-/oagen-emitters-0.7.4.tgz", + "integrity": "sha512-y+SyKirUvvapK2niqFp7uWXA6ii+kiErTDNCtHsDHQoWAQYzkR3/b9/v9PavA8eNxZHyri33AWNDCrzpIbUW2Q==", "license": "MIT", "dependencies": { - "@workos/oagen": "^0.12.0" + "@workos/oagen": "^0.15.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", diff --git a/package.json b/package.json index c305f46..f5800c1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@workos/oagen": "^0.15.0", - "@workos/oagen-emitters": "^0.7.3", + "@workos/oagen-emitters": "^0.7.4", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, From f77defe41575c7afd1120ac1b4fd0a72486b5d76 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Sun, 3 May 2026 11:08:57 -0400 Subject: [PATCH 6/9] fix: bump deps --- package-lock.json | 18 +++++++++--------- package.json | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index a7db366..1ad69aa 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.15.0", - "@workos/oagen-emitters": "^0.7.4", + "@workos/oagen": "^0.16.0", + "@workos/oagen-emitters": "^0.7.5", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, @@ -518,9 +518,9 @@ } }, "node_modules/@workos/oagen": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@workos/oagen/-/oagen-0.15.0.tgz", - "integrity": "sha512-U1al58616PCGNBbapY8NZJ5pFenvCZQf4CaPScLakG4edUJAzZX5yOThAs4nDRIMUJJT9dRcLJjeUaH8WN8Y4Q==", + "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,12 +547,12 @@ } }, "node_modules/@workos/oagen-emitters": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@workos/oagen-emitters/-/oagen-emitters-0.7.4.tgz", - "integrity": "sha512-y+SyKirUvvapK2niqFp7uWXA6ii+kiErTDNCtHsDHQoWAQYzkR3/b9/v9PavA8eNxZHyri33AWNDCrzpIbUW2Q==", + "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.15.0" + "@workos/oagen": "^0.16.0" }, "engines": { "node": ">=24.10.0" diff --git a/package.json b/package.json index f5800c1..ac44904 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "prepare": "husky" }, "dependencies": { - "@workos/oagen": "^0.15.0", - "@workos/oagen-emitters": "^0.7.4", + "@workos/oagen": "^0.16.0", + "@workos/oagen-emitters": "^0.7.5", "js-yaml": "^4.1.1", "openapi-to-postmanv2": "^6.0.1" }, From 7a606dd9af1bf747511ebaf92f280c04ddaeeadd Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Sun, 3 May 2026 11:17:31 -0400 Subject: [PATCH 7/9] patch CLI command --- .github/workflows/validate-sdks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-sdks.yml b/.github/workflows/validate-sdks.yml index 68bcbc7..91ca2c0 100644 --- a/.github/workflows/validate-sdks.yml +++ b/.github/workflows/validate-sdks.yml @@ -184,7 +184,7 @@ jobs: 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 + 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" From 9c91a596b28d0dd78b301505b970aa9c1ea06fe2 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Sun, 3 May 2026 11:47:22 -0400 Subject: [PATCH 8/9] feat: tabbed SDK diff report with file-category filters Replace the single combined diff2html-cli render with a custom builder that emits one tab per language and lets reviewers hide tests and the .oagen-manifest.json so they can focus on real code changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/validate-sdks.yml | 26 ++-- package-lock.json | 75 +++++++++ package.json | 1 + scripts/build-sdk-diff-report.mjs | 234 ++++++++++++++++++++++++++++ 4 files changed, 320 insertions(+), 16 deletions(-) create mode 100644 scripts/build-sdk-diff-report.mjs diff --git a/.github/workflows/validate-sdks.yml b/.github/workflows/validate-sdks.yml index 91ca2c0..1a67e58 100644 --- a/.github/workflows/validate-sdks.yml +++ b/.github/workflows/validate-sdks.yml @@ -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' diff --git a/package-lock.json b/package-lock.json index 1ad69aa..887590c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,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" @@ -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", @@ -769,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", @@ -919,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", @@ -1067,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", @@ -1278,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 ac44904..96bbe4b 100644 --- a/package.json +++ b/package.json @@ -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..70bfa62 --- /dev/null +++ b/scripts/build-sdk-diff-report.mjs @@ -0,0 +1,234 @@ +#!/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', + }); + + blocks.push(`
${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 ``; + }) + .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(); From c9d1e0f2ff5c9a0d630129167dd0c9aa3bd6ee1c Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Sun, 3 May 2026 11:57:12 -0400 Subject: [PATCH 9/9] feat: serve SDK diff report from gh-pages with permalinks Publish each PR's report to gh-pages under pr-/ so reviewers can open it in a browser without downloading the artifact zip. PR comment now links straight to the Pages URL (artifact link remains as the fallback for fork PRs that can't write to the branch). Diff report itself gains stable hash anchors: each tab is addressable via #, and each file gets a permalink icon backed by an id="" so a link like #dotnet/src/WorkOS/ApiKeys.cs activates the right tab and scrolls to the file. Cleanup workflow removes pr-/ from gh-pages on PR close. Note: requires GitHub Pages to be enabled with source = gh-pages branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/cleanup-pr-preview.yml | 45 ++++++++++++++++++++ .github/workflows/validate-sdks.yml | 46 +++++++++++++++++++- scripts/build-sdk-diff-report.mjs | 53 ++++++++++++++++++++---- scripts/sdk-compat-pr-comment.mjs | 13 ++++-- 4 files changed, 146 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/cleanup-pr-preview.yml 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 1a67e58..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 @@ -192,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: | @@ -201,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/scripts/build-sdk-diff-report.mjs b/scripts/build-sdk-diff-report.mjs index 70bfa62..bbf1d01 100644 --- a/scripts/build-sdk-diff-report.mjs +++ b/scripts/build-sdk-diff-report.mjs @@ -73,7 +73,11 @@ function renderLanguageBody(language, diffText) { matching: 'lines', }); - blocks.push(`
${fileHtml}
`); + const anchorId = filePath; + const header = `
${escapeHtml(filePath)}
`; + blocks.push( + `
${header}${fileHtml}
`, + ); } return { fileCount: files.length, counts, body: blocks.join('\n') }; @@ -107,7 +111,7 @@ function buildHtml(languageReports) { const tabs = sortedLanguages .map((entry, idx) => { const totals = `${entry.fileCount} file${entry.fileCount === 1 ? '' : 's'}`; - return ``; + return `${escapeHtml(entry.language)} ${totals}`; }) .join('\n'); @@ -133,10 +137,15 @@ function buildHtml(languageReports) { .filters { display: flex; gap: 12px; align-items: center; font-size: 13px; } .filters label { display: inline-flex; gap: 6px; align-items: center; cursor: pointer; user-select: none; } .tabs { display: flex; gap: 4px; flex-wrap: wrap; padding: 0 24px; background: #fff; border-bottom: 1px solid #d0d7de; } - .tab { background: transparent; border: none; padding: 10px 16px; font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; color: #57606a; } + .tab { background: transparent; border: none; padding: 10px 16px; font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; color: #57606a; text-decoration: none; } .tab:hover { color: #1f2328; } .tab.active { color: #1f2328; border-bottom-color: #fd8c73; font-weight: 600; } .tab-count { color: #8c959f; font-weight: 400; font-size: 11px; margin-left: 4px; } + .diff-file-header { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: #f6f8fa; border: 1px solid #d0d7de; border-bottom: none; border-radius: 6px 6px 0 0; font-size: 12px; } + .diff-file-header .permalink { text-decoration: none; opacity: 0.5; } + .diff-file-header .permalink:hover { opacity: 1; } + .diff-file-header .permalink-path { color: #57606a; } + .diff-file:target .diff-file-header { background: #fff8c5; } main { padding: 16px 24px; } .tab-panel { display: none; } .tab-panel.active { display: block; } @@ -170,12 +179,30 @@ ${panels} (function () { const tabs = document.querySelectorAll('.tab'); const panels = document.querySelectorAll('.tab-panel'); + const languages = Array.from(tabs).map((t) => t.dataset.tab); + + function activateTab(language) { + if (!languages.includes(language)) return false; + tabs.forEach((t) => t.classList.toggle('active', t.dataset.tab === language)); + panels.forEach((p) => p.classList.toggle('active', p.dataset.panel === language)); + updateEmptyState(); + return true; + } + + function languageFromHash(rawHash) { + if (!rawHash) return null; + const decoded = decodeURIComponent(rawHash.replace(/^#/, '')); + const first = decoded.split('/')[0]; + return languages.includes(first) ? first : null; + } + tabs.forEach((tab) => { - tab.addEventListener('click', () => { + tab.addEventListener('click', (event) => { + event.preventDefault(); const target = tab.dataset.tab; - tabs.forEach((t) => t.classList.toggle('active', t === tab)); - panels.forEach((p) => p.classList.toggle('active', p.dataset.panel === target)); - updateEmptyState(); + if (activateTab(target)) { + history.replaceState(null, '', '#' + encodeURIComponent(target)); + } }); }); @@ -196,6 +223,18 @@ ${panels} panel.classList.toggle('all-hidden', !visible); }); } + + // Permalink-driven activation: switch to the right tab on initial load, + // then again whenever the hash changes (browser back/forward, manual edit, + // clicking a per-file permalink that lands in another tab). + function syncFromHash() { + const language = languageFromHash(location.hash); + if (language) activateTab(language); + } + + window.addEventListener('hashchange', syncFromHash); + syncFromHash(); + updateEmptyState(); })(); 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'); }