Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/policy/mount-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export const mountRules: Record<string, string> = {
// Org-scoped authorized applications -> Organizations
OrganizationsAuthorizedApplications: 'Organizations',

// Org-scoped IT contacts -> ItContacts (own service, like Groups)
OrganizationsItContacts: 'ItContacts',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this as a top-level mount if we created this as a subresource? Should we create it as a top-level resource in the API instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mount only changes the generated SDK surface, not the API — the HTTP paths stay /organizations/{organization_id}/it_contacts either way.

Precedent for keeping it top-level: Groups is exactly the same shape (/organizations/{organizationId}/groups, .../groups/{groupId}/organization-memberships) and is mounted as its own Groups service, with organizationId passed as an option — workos.groups.listGroups({ organizationId }). Org API keys and authorized applications go the other way (mounted onto ApiKeys / Organizations), which is why the policy needs an explicit rule here. IT contacts have their own object type (it_contact), their own lifecycle and their own actions (invite/revoke), so they read more like Groups than like organizations.listAuthorizedApplications, and mounting them on Organizations would give workos.organizations.listItContacts(...) alongside unrelated org CRUD. jonatas confirmed workos.itContacts.* as the desired surface.

Making it a top-level resource in the API (/it_contacts?organization_id=...) is a bigger, separate call — it would break the shipped endpoints, and no SDK exposes them yet so nothing forces it now. Happy to switch this to Organizations (or hold pending an API change) if you'd rather; that's just editing these two policy entries and regenerating workos/workos-node#1681.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More-so what I'm saying is it seems like we want it to be both a top-level resource and a sub-resource, but feels like we should choose one.

@jonatascastro12 jonatascastro12 Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... It needs to be a subresource of organization.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it — the inconsistency you're pointing at is real: API says subresource, this policy says top-level.

Two consistent options:

  1. Keep the API as-is and mount on Organizationsworkos.organizations.listItContacts({ organizationId }). Two-line change here plus a regen of feat: Add IT Contacts API workos-node#1681.
  2. Make it top-level in the API too (/it_contacts with organization_id) and keep this mount. That's a breaking change to already-shipped endpoints, so it needs its own decision — though no SDK exposes IT contacts yet, so the blast radius is limited to direct API users.

Worth noting Groups sits in the same "inconsistent" spot today (/organizations/{organizationId}/groups mounted as top-level Groups), so option 1 makes IT contacts stricter than the closest existing precedent. I don't have a stake either way — @jonatascastro12 asked for workos.itContacts.*, so I'll follow whichever you two land on. Tell me which and I'll push it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the API as-is and mount on Organizations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — option 1 is pushed here (OrganizationsItContacts: 'Organizations', commit 7739c4b), and workos/workos-node#1681 now exposes workos.organizations.listItContacts/createItContact/deleteItContact/inviteItContact/revokeItContact({ organizationId, ... }). CI green on both.


// User Management sub-services -> UserManagement, except the
// OrganizationMembership family (longer wildcard prefix wins) and the three
// exact entries below/above (exact always beats a wildcard):
Expand Down
15 changes: 15 additions & 0 deletions src/policy/operation-hints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ export const operationHints: Record<string, OperationHint> = {
name: 'list_authorized_applications',
},

// -- IT contacts (mounted on ItContacts) --------------------------------------
// Drop the `organization` prefix the path would otherwise produce, and name
// the invite/revoke sub-resources after the action.
'GET /organizations/{organization_id}/it_contacts': { name: 'list_it_contacts' },
'POST /organizations/{organization_id}/it_contacts': { name: 'create_it_contact' },
'DELETE /organizations/{organization_id}/it_contacts/{contact_id}': {
name: 'delete_it_contact',
},
'POST /organizations/{organization_id}/it_contacts/{contact_id}/invite': {
name: 'invite_it_contact',
},
'POST /organizations/{organization_id}/it_contacts/{contact_id}/revoke': {
name: 'revoke_it_contact',
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the change above, what do the SDK methods end up looking like?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node (other langs get the same names in their own casing):

await workos.organizations.listItContacts({ organizationId });
await workos.organizations.createItContact({ organizationId, email });
await workos.organizations.deleteItContact({ organizationId, contactId });
await workos.organizations.inviteItContact({ organizationId, contactId, intents: ['sso'] });
await workos.organizations.revokeItContact({ organizationId, contactId });

Without the hints they'd be workos.organizationsItContacts.listOrganizationItContacts(...) etc. See workos/workos-node#1681 for the actual implementation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we do for organization domains currently?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Organization domains are the other shape: the API itself is top-level, not nested — POST /organization_domains (with organization_id in the body), GET|DELETE /organization_domains/{id}, POST /organization_domains/{id}/verify — so they need no mount rule or hints, and the SDK gets workos.organizationDomains.createOrganizationDomain({ organizationId, domain }) / getOrganizationDomain(id) / verifyOrganizationDomain(id) / deleteOrganizationDomain(id).

IT contacts can't follow that without changing the shipped API paths, which is the option we ruled out above. The closest nested precedent is Groups (/organizations/{organizationId}/groups), which is mounted top-level as workos.groups.* — this PR deliberately doesn't follow that one per jonatas' call to keep the SDK consistent with the API hierarchy.


// -- External ID lookups (not derivable from path) ----------------------------
'GET /organizations/external_id/{external_id}': { name: 'get_organization_by_external_id' },
'GET /user_management/users/external_id/{external_id}': { name: 'get_user_by_external_id' },
Expand Down