diff --git a/src/api-keys/api-keys.ts b/src/api-keys/api-keys.ts index 78b074996..b0379eda3 100644 --- a/src/api-keys/api-keys.ts +++ b/src/api-keys/api-keys.ts @@ -23,6 +23,7 @@ import { } from './interfaces/validate-api-key.interface'; import { deserializeValidateApiKeyResponse } from './serializers/validate-api-key.serializer'; import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class ApiKeys { constructor(private readonly workos: WorkOS) {} @@ -60,7 +61,7 @@ export class ApiKeys { * @throws {NotFoundException} 404 */ async deleteApiKey(id: string): Promise { - await this.workos.delete(`/api_keys/${id}`); + await this.workos.delete(`/api_keys/${encodePathParameter(id)}`); } /** @@ -84,14 +85,14 @@ export class ApiKeys { return new AutoPaginatable( await fetchAndDeserialize( this.workos, - `/organizations/${organizationId}/api_keys`, + `/organizations/${encodePathParameter(organizationId)}/api_keys`, deserializeApiKey, paginationOptions, ), (params) => fetchAndDeserialize( this.workos, - `/organizations/${organizationId}/api_keys`, + `/organizations/${encodePathParameter(organizationId)}/api_keys`, deserializeApiKey, params, ), @@ -120,7 +121,7 @@ export class ApiKeys { const { organizationId } = options; const { data } = await this.workos.post( - `/organizations/${organizationId}/api_keys`, + `/organizations/${encodePathParameter(organizationId)}/api_keys`, serializeCreateOrganizationApiKeyOptions(options), requestOptions, ); diff --git a/src/audit-logs/audit-logs.ts b/src/audit-logs/audit-logs.ts index b4aa4724b..8f1068488 100644 --- a/src/audit-logs/audit-logs.ts +++ b/src/audit-logs/audit-logs.ts @@ -27,6 +27,7 @@ import { serializeCreateAuditLogEventOptions, serializeCreateAuditLogSchemaOptions, } from './serializers'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class AuditLogs { constructor(private readonly workos: WorkOS) {} @@ -102,7 +103,7 @@ export class AuditLogs { */ async getExport(auditLogExportId: string): Promise { const { data } = await this.workos.get( - `/audit_logs/exports/${auditLogExportId}`, + `/audit_logs/exports/${encodePathParameter(auditLogExportId)}`, ); return deserializeAuditLogExport(data); @@ -121,7 +122,7 @@ export class AuditLogs { options: CreateAuditLogSchemaRequestOptions = {}, ): Promise { const { data } = await this.workos.post( - `/audit_logs/actions/${schema.action}/schemas`, + `/audit_logs/actions/${encodePathParameter(schema.action)}/schemas`, serializeCreateAuditLogSchemaOptions(schema), options, ); @@ -133,7 +134,7 @@ export class AuditLogs { action: string, options?: PaginationOptions, ): Promise> { - const endpoint = `/audit_logs/actions/${action}/schemas`; + const endpoint = `/audit_logs/actions/${encodePathParameter(action)}/schemas`; return new AutoPaginatable( await fetchAndDeserialize( diff --git a/src/authorization/authorization.ts b/src/authorization/authorization.ts index c76e69c51..abbe2c949 100644 --- a/src/authorization/authorization.ts +++ b/src/authorization/authorization.ts @@ -97,6 +97,7 @@ import { AuthorizationOrganizationMembershipResponse, } from '../user-management/interfaces/organization-membership.interface'; import { deserializeAuthorizationOrganizationMembership } from '../user-management/serializers/organization-membership.serializer'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class Authorization { constructor(private readonly workos: WorkOS) {} @@ -155,7 +156,7 @@ export class Authorization { */ async getEnvironmentRole(slug: string): Promise { const { data } = await this.workos.get( - `/authorization/roles/${slug}`, + `/authorization/roles/${encodePathParameter(slug)}`, ); return deserializeEnvironmentRole(data); } @@ -181,7 +182,7 @@ export class Authorization { options: UpdateEnvironmentRoleOptions, ): Promise { const { data } = await this.workos.patch( - `/authorization/roles/${slug}`, + `/authorization/roles/${encodePathParameter(slug)}`, serializeUpdateEnvironmentRoleOptions(options), ); return deserializeEnvironmentRole(data); @@ -208,7 +209,7 @@ export class Authorization { options: SetEnvironmentRolePermissionsOptions, ): Promise { const { data } = await this.workos.put( - `/authorization/roles/${slug}/permissions`, + `/authorization/roles/${encodePathParameter(slug)}/permissions`, { permissions: options.permissions }, ); return deserializeEnvironmentRole(data); @@ -235,7 +236,7 @@ export class Authorization { options: AddEnvironmentRolePermissionOptions, ): Promise { const { data } = await this.workos.post( - `/authorization/roles/${slug}/permissions`, + `/authorization/roles/${encodePathParameter(slug)}/permissions`, { slug: options.permissionSlug }, ); return deserializeEnvironmentRole(data); @@ -263,7 +264,7 @@ export class Authorization { options: CreateOrganizationRoleOptions, ): Promise { const { data } = await this.workos.post( - `/authorization/organizations/${organizationId}/roles`, + `/authorization/organizations/${encodePathParameter(organizationId)}/roles`, serializeCreateOrganizationRoleOptions(options), ); return deserializeOrganizationRole(data); @@ -284,7 +285,7 @@ export class Authorization { */ async listOrganizationRoles(organizationId: string): Promise { const { data } = await this.workos.get( - `/authorization/organizations/${organizationId}/roles`, + `/authorization/organizations/${encodePathParameter(organizationId)}/roles`, ); return { object: 'list', @@ -315,7 +316,7 @@ export class Authorization { slug: string, ): Promise { const { data } = await this.workos.get( - `/authorization/organizations/${organizationId}/roles/${slug}`, + `/authorization/organizations/${encodePathParameter(organizationId)}/roles/${encodePathParameter(slug)}`, ); return deserializeRole(data); } @@ -347,7 +348,7 @@ export class Authorization { options: UpdateOrganizationRoleOptions, ): Promise { const { data } = await this.workos.patch( - `/authorization/organizations/${organizationId}/roles/${slug}`, + `/authorization/organizations/${encodePathParameter(organizationId)}/roles/${encodePathParameter(slug)}`, serializeUpdateOrganizationRoleOptions(options), ); return deserializeOrganizationRole(data); @@ -378,7 +379,7 @@ export class Authorization { slug: string, ): Promise { await this.workos.delete( - `/authorization/organizations/${organizationId}/roles/${slug}`, + `/authorization/organizations/${encodePathParameter(organizationId)}/roles/${encodePathParameter(slug)}`, ); } @@ -408,7 +409,7 @@ export class Authorization { options: SetOrganizationRolePermissionsOptions, ): Promise { const { data } = await this.workos.put( - `/authorization/organizations/${organizationId}/roles/${slug}/permissions`, + `/authorization/organizations/${encodePathParameter(organizationId)}/roles/${encodePathParameter(slug)}/permissions`, { permissions: options.permissions }, ); return deserializeOrganizationRole(data); @@ -441,7 +442,7 @@ export class Authorization { options: AddOrganizationRolePermissionOptions, ): Promise { const { data } = await this.workos.post( - `/authorization/organizations/${organizationId}/roles/${slug}/permissions`, + `/authorization/organizations/${encodePathParameter(organizationId)}/roles/${encodePathParameter(slug)}/permissions`, { slug: options.permissionSlug }, ); return deserializeOrganizationRole(data); @@ -476,7 +477,7 @@ export class Authorization { options: RemoveOrganizationRolePermissionOptions, ): Promise { await this.workos.delete( - `/authorization/organizations/${organizationId}/roles/${slug}/permissions/${options.permissionSlug}`, + `/authorization/organizations/${encodePathParameter(organizationId)}/roles/${encodePathParameter(slug)}/permissions/${encodePathParameter(options.permissionSlug)}`, ); } @@ -544,7 +545,7 @@ export class Authorization { */ async getPermission(slug: string): Promise { const { data } = await this.workos.get( - `/authorization/permissions/${slug}`, + `/authorization/permissions/${encodePathParameter(slug)}`, ); return deserializePermission(data); } @@ -569,7 +570,7 @@ export class Authorization { options: UpdatePermissionOptions, ): Promise { const { data } = await this.workos.patch( - `/authorization/permissions/${slug}`, + `/authorization/permissions/${encodePathParameter(slug)}`, serializeUpdatePermissionOptions(options), ); return deserializePermission(data); @@ -589,7 +590,9 @@ export class Authorization { * @throws {NotFoundException} 404 */ async deletePermission(slug: string): Promise { - await this.workos.delete(`/authorization/permissions/${slug}`); + await this.workos.delete( + `/authorization/permissions/${encodePathParameter(slug)}`, + ); } /** @@ -608,7 +611,7 @@ export class Authorization { */ async getResource(resourceId: string): Promise { const { data } = await this.workos.get( - `/authorization/resources/${resourceId}`, + `/authorization/resources/${encodePathParameter(resourceId)}`, ); return deserializeAuthorizationResource(data); } @@ -651,7 +654,7 @@ export class Authorization { options: UpdateAuthorizationResourceOptions, ): Promise { const { data } = await this.workos.patch( - `/authorization/resources/${options.resourceId}`, + `/authorization/resources/${encodePathParameter(options.resourceId)}`, serializeUpdateResourceOptions(options), ); return deserializeAuthorizationResource(data); @@ -680,7 +683,10 @@ export class Authorization { ? { cascade_delete: cascadeDelete.toString() } : undefined; - await this.workos.delete(`/authorization/resources/${resourceId}`, query); + await this.workos.delete( + `/authorization/resources/${encodePathParameter(resourceId)}`, + query, + ); } /** @@ -749,7 +755,7 @@ export class Authorization { ): Promise { const { organizationId, resourceTypeSlug, externalId } = options; const { data } = await this.workos.get( - `/authorization/organizations/${organizationId}/resources/${resourceTypeSlug}/${externalId}`, + `/authorization/organizations/${encodePathParameter(organizationId)}/resources/${encodePathParameter(resourceTypeSlug)}/${encodePathParameter(externalId)}`, ); return deserializeAuthorizationResource(data); } @@ -786,7 +792,7 @@ export class Authorization { ): Promise { const { organizationId, resourceTypeSlug, externalId } = options; const { data } = await this.workos.patch( - `/authorization/organizations/${organizationId}/resources/${resourceTypeSlug}/${externalId}`, + `/authorization/organizations/${encodePathParameter(organizationId)}/resources/${encodePathParameter(resourceTypeSlug)}/${encodePathParameter(externalId)}`, serializeUpdateResourceByExternalIdOptions(options), ); return deserializeAuthorizationResource(data); @@ -829,7 +835,7 @@ export class Authorization { : undefined; await this.workos.delete( - `/authorization/organizations/${organizationId}/resources/${resourceTypeSlug}/${externalId}`, + `/authorization/organizations/${encodePathParameter(organizationId)}/resources/${encodePathParameter(resourceTypeSlug)}/${encodePathParameter(externalId)}`, query, ); } @@ -848,7 +854,7 @@ export class Authorization { options: AuthorizationCheckOptions, ): Promise { const { data } = await this.workos.post( - `/authorization/organization_memberships/${options.organizationMembershipId}/check`, + `/authorization/organization_memberships/${encodePathParameter(options.organizationMembershipId)}/check`, serializeAuthorizationCheckOptions(options), ); return data; @@ -872,7 +878,7 @@ export class Authorization { options: ListRoleAssignmentsOptions, ): Promise> { const { organizationMembershipId, ...queryOptions } = options; - const endpoint = `/authorization/organization_memberships/${organizationMembershipId}/role_assignments`; + const endpoint = `/authorization/organization_memberships/${encodePathParameter(organizationMembershipId)}/role_assignments`; const serializedOptions = serializeListRoleAssignmentsOptions(queryOptions); return new AutoPaginatable( await fetchAndDeserialize( @@ -910,7 +916,7 @@ export class Authorization { options: ListRoleAssignmentsForResourceOptions, ): Promise> { const { resourceId, ...queryOptions } = options; - const endpoint = `/authorization/resources/${resourceId}/role_assignments`; + const endpoint = `/authorization/resources/${encodePathParameter(resourceId)}/role_assignments`; const serializedOptions = serializeListRoleAssignmentsForResourceOptions(queryOptions); return new AutoPaginatable( @@ -960,7 +966,7 @@ export class Authorization { ): Promise> { const { organizationId, resourceTypeSlug, externalId, ...queryOptions } = options; - const endpoint = `/authorization/organizations/${organizationId}/resources/${resourceTypeSlug}/${externalId}/role_assignments`; + const endpoint = `/authorization/organizations/${encodePathParameter(organizationId)}/resources/${encodePathParameter(resourceTypeSlug)}/${encodePathParameter(externalId)}/role_assignments`; const serializedOptions = serializeListRoleAssignmentsForResourceOptions(queryOptions); return new AutoPaginatable( @@ -993,7 +999,7 @@ export class Authorization { */ async assignRole(options: AssignRoleOptions): Promise { const { data } = await this.workos.post( - `/authorization/organization_memberships/${options.organizationMembershipId}/role_assignments`, + `/authorization/organization_memberships/${encodePathParameter(options.organizationMembershipId)}/role_assignments`, serializeAssignRoleOptions(options), ); return deserializeRoleAssignment(data); @@ -1011,7 +1017,7 @@ export class Authorization { */ async removeRole(options: RemoveRoleOptions): Promise { await this.workos.deleteWithBody( - `/authorization/organization_memberships/${options.organizationMembershipId}/role_assignments`, + `/authorization/organization_memberships/${encodePathParameter(options.organizationMembershipId)}/role_assignments`, serializeRemoveRoleOptions(options), ); } @@ -1038,7 +1044,7 @@ export class Authorization { options: RemoveRoleAssignmentOptions, ): Promise { await this.workos.delete( - `/authorization/organization_memberships/${options.organizationMembershipId}/role_assignments/${options.roleAssignmentId}`, + `/authorization/organization_memberships/${encodePathParameter(options.organizationMembershipId)}/role_assignments/${encodePathParameter(options.roleAssignmentId)}`, ); } @@ -1060,7 +1066,7 @@ export class Authorization { options: ListGroupRoleAssignmentsOptions, ): Promise> { const { groupId, ...paginationOptions } = options; - const endpoint = `/authorization/groups/${groupId}/role_assignments`; + const endpoint = `/authorization/groups/${encodePathParameter(groupId)}/role_assignments`; return new AutoPaginatable( await fetchAndDeserialize< GroupRoleAssignmentResponse, @@ -1105,7 +1111,7 @@ export class Authorization { options: GetGroupRoleAssignmentOptions, ): Promise { const { data } = await this.workos.get( - `/authorization/groups/${options.groupId}/role_assignments/${options.roleAssignmentId}`, + `/authorization/groups/${encodePathParameter(options.groupId)}/role_assignments/${encodePathParameter(options.roleAssignmentId)}`, ); return deserializeGroupRoleAssignment(data); } @@ -1125,7 +1131,7 @@ export class Authorization { options: CreateGroupRoleAssignmentOptions, ): Promise { const { data } = await this.workos.post( - `/authorization/groups/${options.groupId}/role_assignments`, + `/authorization/groups/${encodePathParameter(options.groupId)}/role_assignments`, serializeCreateGroupRoleAssignmentOptions(options), ); return deserializeGroupRoleAssignment(data); @@ -1154,7 +1160,7 @@ export class Authorization { options: RemoveGroupRoleAssignmentOptions, ): Promise { await this.workos.delete( - `/authorization/groups/${options.groupId}/role_assignments/${options.roleAssignmentId}`, + `/authorization/groups/${encodePathParameter(options.groupId)}/role_assignments/${encodePathParameter(options.roleAssignmentId)}`, ); } @@ -1172,7 +1178,7 @@ export class Authorization { options: RemoveGroupRoleAssignmentsOptions, ): Promise { await this.workos.deleteWithBody( - `/authorization/groups/${options.groupId}/role_assignments`, + `/authorization/groups/${encodePathParameter(options.groupId)}/role_assignments`, serializeRemoveGroupRoleAssignmentsOptions(options), ); } @@ -1193,7 +1199,7 @@ export class Authorization { const { data } = await this.workos.put< ListResponse >( - `/authorization/groups/${options.groupId}/role_assignments`, + `/authorization/groups/${encodePathParameter(options.groupId)}/role_assignments`, serializeReplaceGroupRoleAssignmentsOptions(options), ); return deserializeList(data, deserializeGroupRoleAssignment); @@ -1221,7 +1227,7 @@ export class Authorization { options: ListResourcesForMembershipOptions, ): Promise> { const { organizationMembershipId } = options; - const endpoint = `/authorization/organization_memberships/${organizationMembershipId}/resources`; + const endpoint = `/authorization/organization_memberships/${encodePathParameter(organizationMembershipId)}/resources`; const serializedOptions = serializeListResourcesForMembershipOptions(options); return new AutoPaginatable( @@ -1258,7 +1264,7 @@ export class Authorization { options: ListMembershipsForResourceOptions, ): Promise> { const { resourceId } = options; - const endpoint = `/authorization/resources/${resourceId}/organization_memberships`; + const endpoint = `/authorization/resources/${encodePathParameter(resourceId)}/organization_memberships`; const serializedOptions = serializeListMembershipsForResourceOptions(options); return new AutoPaginatable( @@ -1315,7 +1321,7 @@ export class Authorization { options: ListMembershipsForResourceByExternalIdOptions, ): Promise> { const { organizationId, resourceTypeSlug, externalId } = options; - const endpoint = `/authorization/organizations/${organizationId}/resources/${resourceTypeSlug}/${externalId}/organization_memberships`; + const endpoint = `/authorization/organizations/${encodePathParameter(organizationId)}/resources/${encodePathParameter(resourceTypeSlug)}/${encodePathParameter(externalId)}/organization_memberships`; const serializedOptions = serializeListMembershipsForResourceOptions(options); return new AutoPaginatable( @@ -1366,7 +1372,7 @@ export class Authorization { options: ListEffectivePermissionsOptions, ): Promise> { const { organizationMembershipId, resourceId } = options; - const endpoint = `/authorization/resources/${resourceId}/organization_memberships/${organizationMembershipId}/permissions`; + const endpoint = `/authorization/resources/${encodePathParameter(resourceId)}/organization_memberships/${encodePathParameter(organizationMembershipId)}/permissions`; const serializedOptions = serializeListEffectivePermissionsOptions(options); return new AutoPaginatable( await fetchAndDeserialize( @@ -1400,7 +1406,7 @@ export class Authorization { options: ListEffectivePermissionsByExternalIdOptions, ): Promise> { const { organizationMembershipId, resourceTypeSlug, externalId } = options; - const endpoint = `/authorization/organization_memberships/${organizationMembershipId}/resources/${resourceTypeSlug}/${externalId}/permissions`; + const endpoint = `/authorization/organization_memberships/${encodePathParameter(organizationMembershipId)}/resources/${encodePathParameter(resourceTypeSlug)}/${encodePathParameter(externalId)}/permissions`; const serializedOptions = serializeListEffectivePermissionsOptions(options); return new AutoPaginatable( await fetchAndDeserialize( diff --git a/src/common/utils/encode-path-parameter.spec.ts b/src/common/utils/encode-path-parameter.spec.ts new file mode 100644 index 000000000..0dc72fc52 --- /dev/null +++ b/src/common/utils/encode-path-parameter.spec.ts @@ -0,0 +1,39 @@ +import { encodePathParameter } from './encode-path-parameter'; + +describe('encodePathParameter', () => { + it('leaves ordinary identifiers unchanged', () => { + expect(encodePathParameter('user_01ABC')).toBe('user_01ABC'); + expect(encodePathParameter('auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ')).toBe( + 'auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ', + ); + }); + + it('preserves colons used by RBAC slugs', () => { + expect(encodePathParameter('users:read')).toBe('users:read'); + expect(encodePathParameter('members:invite')).toBe('members:invite'); + }); + + it('encodes path traversal and metacharacters so one param stays one segment', () => { + expect(encodePathParameter('../../user_management/users/user_01ABC')).toBe( + '..%2F..%2Fuser_management%2Fusers%2Fuser_01ABC', + ); + expect(encodePathParameter('a/b')).toBe('a%2Fb'); + expect(encodePathParameter('a?b')).toBe('a%3Fb'); + expect(encodePathParameter('a#b')).toBe('a%23b'); + expect(encodePathParameter('%2e%2e')).toBe('%252e%252e'); + }); + + it('rejects dot-only segments the URL parser would collapse', () => { + // `encodeURIComponent` leaves these unchanged and `new URL()` removes them + // as relative path segments, so a non-terminal template (e.g. + // `/feature-flags/${slug}/enable`) would retarget the request. + expect(() => encodePathParameter('.')).toThrow(TypeError); + expect(() => encodePathParameter('..')).toThrow(TypeError); + }); + + it('allows segments that merely contain dots', () => { + expect(encodePathParameter('...')).toBe('...'); + expect(encodePathParameter('..foo')).toBe('..foo'); + expect(encodePathParameter('v1.2.3')).toBe('v1.2.3'); + }); +}); diff --git a/src/common/utils/encode-path-parameter.ts b/src/common/utils/encode-path-parameter.ts new file mode 100644 index 000000000..a524b4157 --- /dev/null +++ b/src/common/utils/encode-path-parameter.ts @@ -0,0 +1,41 @@ +/** + * Encode a caller-supplied identifier for safe interpolation into a single URL + * path segment. + * + * SDK path templates interpolate caller-supplied identifiers (user IDs, + * invitation tokens, MFA factor IDs, organization IDs, external IDs, role and + * permission slugs, ...) into the request path. Without encoding, a value such + * as `../../user_management/users/user_01ABC` would be resolved by the WHATWG + * `URL` parser (see `HttpClient.getResourceURL`) into a different API path, + * letting an attacker who influences one identifier retarget the request to an + * arbitrary same-verb endpoint. `encodeURIComponent` neutralizes this by + * percent-encoding the path/query/fragment metacharacters (`/`, `?`, `#`, ...) + * that enable the injection, keeping one parameter mapped to exactly one path + * segment. + * + * `encodeURIComponent` alone is not sufficient for the two dot-only segments + * `.` and `..`: it leaves them unchanged, and the WHATWG `URL` parser then + * removes them as relative path segments (even when a value like `..` is a + * single segment, a template such as `/feature-flags/${slug}/enable` supplies + * the trailing segment, so `..` still climbs and retargets the request). + * Percent-encoding the dots does not help because the parser also treats the + * `%2e` forms as dot segments. A `.` or `..` is never a valid WorkOS + * identifier, so we fail closed and throw rather than emit an ambiguous path. + * + * The one deviation from `encodeURIComponent` is that a literal `:` is kept + * unescaped. Colons are valid path-segment characters (RFC 3986 `pchar`) and + * are used by WorkOS RBAC slugs (e.g. `users:read`); an interpolated value is + * always preceded by a `/`-delimited segment, so a `:` can never be read as a + * URL scheme. Preserving it keeps the wire format identical for existing slugs. + */ +export function encodePathParameter(value: string): string { + const encoded = encodeURIComponent(value).replace(/%3A/gi, ':'); + + if (encoded === '.' || encoded === '..') { + throw new TypeError( + 'Invalid path parameter: a path parameter must not be "." or "..".', + ); + } + + return encoded; +} diff --git a/src/directory-sync/directory-sync.ts b/src/directory-sync/directory-sync.ts index 021acf0e6..2952d2bbc 100644 --- a/src/directory-sync/directory-sync.ts +++ b/src/directory-sync/directory-sync.ts @@ -20,6 +20,7 @@ import { serializeListDirectoriesOptions, } from './serializers'; import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class DirectorySync { constructor(private readonly workos: WorkOS) {} @@ -69,7 +70,7 @@ export class DirectorySync { */ async getDirectory(id: string): Promise { const { data } = await this.workos.get( - `/directories/${id}`, + `/directories/${encodePathParameter(id)}`, ); return deserializeDirectory(data); @@ -88,7 +89,7 @@ export class DirectorySync { * @throws 403 response from the API. */ async deleteDirectory(id: string) { - await this.workos.delete(`/directories/${id}`); + await this.workos.delete(`/directories/${encodePathParameter(id)}`); } /** @@ -182,7 +183,7 @@ export class DirectorySync { ): Promise> { const { data } = await this.workos.get< DirectoryUserWithGroupsResponse - >(`/directory_users/${user}`); + >(`/directory_users/${encodePathParameter(user)}`); return deserializeDirectoryUserWithGroups(data); } @@ -201,7 +202,7 @@ export class DirectorySync { */ async getGroup(group: string): Promise { const { data } = await this.workos.get( - `/directory_groups/${group}`, + `/directory_groups/${encodePathParameter(group)}`, ); return deserializeDirectoryGroup(data); diff --git a/src/feature-flags/feature-flags.ts b/src/feature-flags/feature-flags.ts index e8e706389..707f72336 100644 --- a/src/feature-flags/feature-flags.ts +++ b/src/feature-flags/feature-flags.ts @@ -13,6 +13,7 @@ import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize'; import { FeatureFlagsRuntimeClient } from './runtime-client'; import { ListOrganizationFeatureFlagsOptions } from '../organizations/interfaces/list-organization-feature-flags-options.interface'; import { ListUserFeatureFlagsOptions } from '../user-management/interfaces/list-user-feature-flags-options.interface'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class FeatureFlags { constructor(private readonly workos: WorkOS) {} @@ -62,7 +63,7 @@ export class FeatureFlags { */ async getFeatureFlag(slug: string): Promise { const { data } = await this.workos.get( - `/feature-flags/${slug}`, + `/feature-flags/${encodePathParameter(slug)}`, ); return deserializeFeatureFlag(data); @@ -82,7 +83,7 @@ export class FeatureFlags { */ async enableFeatureFlag(slug: string): Promise { const { data } = await this.workos.put( - `/feature-flags/${slug}/enable`, + `/feature-flags/${encodePathParameter(slug)}/enable`, {}, ); @@ -103,7 +104,7 @@ export class FeatureFlags { */ async disableFeatureFlag(slug: string): Promise { const { data } = await this.workos.put( - `/feature-flags/${slug}/disable`, + `/feature-flags/${encodePathParameter(slug)}/disable`, {}, ); @@ -122,7 +123,10 @@ export class FeatureFlags { */ async addFlagTarget(options: AddFlagTargetOptions): Promise { const { slug, targetId } = options; - await this.workos.post(`/feature-flags/${slug}/targets/${targetId}`, {}); + await this.workos.post( + `/feature-flags/${encodePathParameter(slug)}/targets/${encodePathParameter(targetId)}`, + {}, + ); } /** @@ -137,7 +141,9 @@ export class FeatureFlags { */ async removeFlagTarget(options: RemoveFlagTargetOptions): Promise { const { slug, targetId } = options; - await this.workos.delete(`/feature-flags/${slug}/targets/${targetId}`); + await this.workos.delete( + `/feature-flags/${encodePathParameter(slug)}/targets/${encodePathParameter(targetId)}`, + ); } /** @@ -156,14 +162,14 @@ export class FeatureFlags { return new AutoPaginatable( await fetchAndDeserialize( this.workos, - `/organizations/${organizationId}/feature-flags`, + `/organizations/${encodePathParameter(organizationId)}/feature-flags`, deserializeFeatureFlag, paginationOptions, ), (params) => fetchAndDeserialize( this.workos, - `/organizations/${organizationId}/feature-flags`, + `/organizations/${encodePathParameter(organizationId)}/feature-flags`, deserializeFeatureFlag, params, ), @@ -186,14 +192,14 @@ export class FeatureFlags { return new AutoPaginatable( await fetchAndDeserialize( this.workos, - `/user_management/users/${userId}/feature-flags`, + `/user_management/users/${encodePathParameter(userId)}/feature-flags`, deserializeFeatureFlag, paginationOptions, ), (params) => fetchAndDeserialize( this.workos, - `/user_management/users/${userId}/feature-flags`, + `/user_management/users/${encodePathParameter(userId)}/feature-flags`, deserializeFeatureFlag, params, ), diff --git a/src/multi-factor-auth/multi-factor-auth.ts b/src/multi-factor-auth/multi-factor-auth.ts index 006ac1222..396d9c4b1 100644 --- a/src/multi-factor-auth/multi-factor-auth.ts +++ b/src/multi-factor-auth/multi-factor-auth.ts @@ -34,6 +34,7 @@ import { import { serializeEnrollAuthFactorOptions } from '../user-management/serializers'; import { deserializeFactorWithSecrets as deserializeUMFactorWithSecrets } from '../user-management/serializers/authentication-factor.serializer'; import { deserializeFactor as deserializeUMFactor } from '../user-management/serializers/authentication-factor.serializer'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class MultiFactorAuth { constructor(private readonly workos: WorkOS) {} @@ -51,7 +52,7 @@ export class MultiFactorAuth { * @throws {NotFoundException} 404 */ async deleteFactor(id: string) { - await this.workos.delete(`/auth/factors/${id}`); + await this.workos.delete(`/auth/factors/${encodePathParameter(id)}`); } /** @@ -68,7 +69,7 @@ export class MultiFactorAuth { */ async getFactor(id: string): Promise { const { data } = await this.workos.get( - `/auth/factors/${id}`, + `/auth/factors/${encodePathParameter(id)}`, ); return deserializeFactor(data); @@ -119,7 +120,7 @@ export class MultiFactorAuth { */ async challengeFactor(options: ChallengeFactorOptions): Promise { const { data } = await this.workos.post( - `/auth/factors/${options.authenticationFactorId}/challenge`, + `/auth/factors/${encodePathParameter(options.authenticationFactorId)}/challenge`, { sms_template: 'smsTemplate' in options ? options.smsTemplate : undefined, @@ -143,7 +144,7 @@ export class MultiFactorAuth { options: VerifyChallengeOptions, ): Promise { const { data } = await this.workos.post( - `/auth/challenges/${options.authenticationChallengeId}/verify`, + `/auth/challenges/${encodePathParameter(options.authenticationChallengeId)}/verify`, { code: options.code, }, @@ -168,7 +169,7 @@ export class MultiFactorAuth { authentication_factor: UMFactorWithSecretsResponse; authentication_challenge: ChallengeResponse; }>( - `/user_management/users/${payload.userId}/auth_factors`, + `/user_management/users/${encodePathParameter(payload.userId)}/auth_factors`, serializeEnrollAuthFactorOptions(payload), ); @@ -197,14 +198,14 @@ export class MultiFactorAuth { return new AutoPaginatable( await fetchAndDeserialize( this.workos, - `/user_management/users/${userId}/auth_factors`, + `/user_management/users/${encodePathParameter(userId)}/auth_factors`, deserializeUMFactor, restOfOptions, ), (params) => fetchAndDeserialize( this.workos, - `/user_management/users/${userId}/auth_factors`, + `/user_management/users/${encodePathParameter(userId)}/auth_factors`, deserializeUMFactor, params, ), diff --git a/src/organization-domains/organization-domains.ts b/src/organization-domains/organization-domains.ts index 67193ddcc..7af136bac 100644 --- a/src/organization-domains/organization-domains.ts +++ b/src/organization-domains/organization-domains.ts @@ -6,6 +6,7 @@ import { } from './interfaces'; import { serializeCreateOrganizationDomainOptions } from './serializers/create-organization-domain-options.serializer'; import { deserializeOrganizationDomain } from './serializers/organization-domain.serializer'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class OrganizationDomains { constructor(private readonly workos: WorkOS) {} @@ -24,7 +25,7 @@ export class OrganizationDomains { */ async getOrganizationDomain(id: string): Promise { const { data } = await this.workos.get( - `/organization_domains/${id}`, + `/organization_domains/${encodePathParameter(id)}`, ); return deserializeOrganizationDomain(data); @@ -44,7 +45,7 @@ export class OrganizationDomains { */ async verifyOrganizationDomain(id: string): Promise { const { data } = await this.workos.post( - `/organization_domains/${id}/verify`, + `/organization_domains/${encodePathParameter(id)}/verify`, {}, ); @@ -83,6 +84,8 @@ export class OrganizationDomains { * @throws {NotFoundException} 404 */ async deleteOrganizationDomain(id: string): Promise { - await this.workos.delete(`/organization_domains/${id}`); + await this.workos.delete( + `/organization_domains/${encodePathParameter(id)}`, + ); } } diff --git a/src/organizations/organizations.ts b/src/organizations/organizations.ts index 4f2053471..75504ea27 100644 --- a/src/organizations/organizations.ts +++ b/src/organizations/organizations.ts @@ -15,6 +15,7 @@ import { } from './serializers'; import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class Organizations { constructor(private readonly workos: WorkOS) {} @@ -84,7 +85,7 @@ export class Organizations { * @throws 403 response from the API. */ async deleteOrganization(id: string) { - await this.workos.delete(`/organizations/${id}`); + await this.workos.delete(`/organizations/${encodePathParameter(id)}`); } /** @@ -101,7 +102,7 @@ export class Organizations { */ async getOrganization(id: string): Promise { const { data } = await this.workos.get( - `/organizations/${id}`, + `/organizations/${encodePathParameter(id)}`, ); return deserializeOrganization(data); @@ -121,7 +122,7 @@ export class Organizations { */ async getOrganizationByExternalId(externalId: string): Promise { const { data } = await this.workos.get( - `/organizations/external_id/${externalId}`, + `/organizations/external_id/${encodePathParameter(externalId)}`, ); return deserializeOrganization(data); @@ -145,7 +146,7 @@ export class Organizations { const { organization: organizationId, ...payload } = options; const { data } = await this.workos.put( - `/organizations/${organizationId}`, + `/organizations/${encodePathParameter(organizationId)}`, serializeUpdateOrganizationOptions(payload), ); diff --git a/src/passwordless/passwordless.ts b/src/passwordless/passwordless.ts index 6f515940f..e420ac93d 100644 --- a/src/passwordless/passwordless.ts +++ b/src/passwordless/passwordless.ts @@ -8,6 +8,7 @@ import { SerializedCreatePasswordlessSessionOptions, } from './interfaces'; import { deserializePasswordlessSession } from './serializers/passwordless-session.serializer'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class Passwordless { constructor(private readonly workos: WorkOS) {} @@ -31,7 +32,7 @@ export class Passwordless { async sendSession(sessionId: string): Promise { const { data } = await this.workos.post( - `/passwordless/sessions/${sessionId}/send`, + `/passwordless/sessions/${encodePathParameter(sessionId)}/send`, {}, ); return data; diff --git a/src/sso/sso.ts b/src/sso/sso.ts index 5ec2e8ebb..79167b2df 100644 --- a/src/sso/sso.ts +++ b/src/sso/sso.ts @@ -23,6 +23,7 @@ import { deserializeProfileAndToken, serializeListConnectionsOptions, } from './serializers'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class SSO { constructor(private readonly workos: WorkOS) {} @@ -70,7 +71,7 @@ export class SSO { * @throws {NotFoundException} 404 */ async deleteConnection(id: string) { - await this.workos.delete(`/connections/${id}`); + await this.workos.delete(`/connections/${encodePathParameter(id)}`); } // @oagen-ignore-start @@ -206,7 +207,7 @@ export class SSO { */ async getConnection(id: string): Promise { const { data } = await this.workos.get( - `/connections/${id}`, + `/connections/${encodePathParameter(id)}`, ); return deserializeConnection(data); diff --git a/src/user-management/user-management.ts b/src/user-management/user-management.ts index 0d61ecc32..084ee6e4a 100644 --- a/src/user-management/user-management.ts +++ b/src/user-management/user-management.ts @@ -168,6 +168,7 @@ import { CookieSession } from './session'; import { getJose } from '../utils/jose'; import { Group, GroupResponse } from '../groups/interfaces'; import { deserializeGroup } from '../groups/serializers'; +import { encodePathParameter } from '../common/utils/encode-path-parameter'; export class UserManagement { // @oagen-ignore-start @@ -241,7 +242,7 @@ export class UserManagement { */ async getUser(userId: string): Promise { const { data } = await this.workos.get( - `/user_management/users/${userId}`, + `/user_management/users/${encodePathParameter(userId)}`, ); return deserializeUser(data); @@ -261,7 +262,7 @@ export class UserManagement { */ async getUserByExternalId(externalId: string): Promise { const { data } = await this.workos.get( - `/user_management/users/external_id/${externalId}`, + `/user_management/users/external_id/${encodePathParameter(externalId)}`, ); return deserializeUser(data); @@ -829,7 +830,7 @@ export class UserManagement { emailVerificationId: string, ): Promise { const { data } = await this.workos.get( - `/user_management/email_verification/${emailVerificationId}`, + `/user_management/email_verification/${encodePathParameter(emailVerificationId)}`, ); return deserializeEmailVerification(data); @@ -848,7 +849,7 @@ export class UserManagement { userId, }: SendVerificationEmailOptions): Promise<{ user: User }> { const { data } = await this.workos.post<{ user: UserResponse }>( - `/user_management/users/${userId}/email_verification/send`, + `/user_management/users/${encodePathParameter(userId)}/email_verification/send`, {}, ); @@ -864,7 +865,7 @@ export class UserManagement { */ async getMagicAuth(magicAuthId: string): Promise { const { data } = await this.workos.get( - `/user_management/magic_auth/${magicAuthId}`, + `/user_management/magic_auth/${encodePathParameter(magicAuthId)}`, ); return deserializeMagicAuth(data); @@ -919,9 +920,12 @@ export class UserManagement { const { data } = await this.workos.post< { user: UserResponse }, SerializedVerifyEmailOptions - >(`/user_management/users/${userId}/email_verification/confirm`, { - code, - }); + >( + `/user_management/users/${encodePathParameter(userId)}/email_verification/confirm`, + { + code, + }, + ); return { user: deserializeUser(data.user) }; } @@ -935,7 +939,7 @@ export class UserManagement { */ async getPasswordReset(passwordResetId: string): Promise { const { data } = await this.workos.get( - `/user_management/password_reset/${passwordResetId}`, + `/user_management/password_reset/${encodePathParameter(passwordResetId)}`, ); return deserializePasswordReset(data); @@ -993,7 +997,7 @@ export class UserManagement { */ async updateUser(payload: UpdateUserOptions): Promise { const { data } = await this.workos.put( - `/user_management/users/${payload.userId}`, + `/user_management/users/${encodePathParameter(payload.userId)}`, serializeUpdateUserOptions(payload), ); @@ -1016,14 +1020,14 @@ export class UserManagement { return new AutoPaginatable( await fetchAndDeserialize( this.workos, - `/user_management/users/${userId}/sessions`, + `/user_management/users/${encodePathParameter(userId)}/sessions`, deserializeSession, options ? serializeListSessionsOptions(options) : undefined, ), (params) => fetchAndDeserialize( this.workos, - `/user_management/users/${userId}/sessions`, + `/user_management/users/${encodePathParameter(userId)}/sessions`, deserializeSession, params, ), @@ -1039,7 +1043,9 @@ export class UserManagement { * @throws {NotFoundException} 404 */ async deleteUser(userId: string) { - await this.workos.delete(`/user_management/users/${userId}`); + await this.workos.delete( + `/user_management/users/${encodePathParameter(userId)}`, + ); } /** @@ -1055,7 +1061,7 @@ export class UserManagement { } const { data } = await this.workos.get( - `/user_management/users/${userId}/identities`, + `/user_management/users/${encodePathParameter(userId)}/identities`, ); return deserializeIdentities(data); @@ -1072,7 +1078,7 @@ export class UserManagement { organizationMembershipId: string, ): Promise { const { data } = await this.workos.get( - `/user_management/organization_memberships/${organizationMembershipId}`, + `/user_management/organization_memberships/${encodePathParameter(organizationMembershipId)}`, ); return deserializeOrganizationMembership(data); @@ -1166,7 +1172,7 @@ export class UserManagement { OrganizationMembershipResponse, SerializedUpdateOrganizationMembershipOptions >( - `/user_management/organization_memberships/${organizationMembershipId}`, + `/user_management/organization_memberships/${encodePathParameter(organizationMembershipId)}`, serializeUpdateOrganizationMembershipOptions(options), ); @@ -1184,7 +1190,7 @@ export class UserManagement { organizationMembershipId: string, ): Promise { await this.workos.delete( - `/user_management/organization_memberships/${organizationMembershipId}`, + `/user_management/organization_memberships/${encodePathParameter(organizationMembershipId)}`, ); } @@ -1206,7 +1212,7 @@ export class UserManagement { organizationMembershipId: string, ): Promise { const { data } = await this.workos.put( - `/user_management/organization_memberships/${organizationMembershipId}/deactivate`, + `/user_management/organization_memberships/${encodePathParameter(organizationMembershipId)}/deactivate`, {}, ); @@ -1231,7 +1237,7 @@ export class UserManagement { organizationMembershipId: string, ): Promise { const { data } = await this.workos.put( - `/user_management/organization_memberships/${organizationMembershipId}/reactivate`, + `/user_management/organization_memberships/${encodePathParameter(organizationMembershipId)}/reactivate`, {}, ); @@ -1242,7 +1248,7 @@ export class UserManagement { options: ListGroupsForOrganizationMembershipOptions, ): Promise> { const { organizationMembershipId, ...paginationOptions } = options; - const endpoint = `/user_management/organization_memberships/${organizationMembershipId}/groups`; + const endpoint = `/user_management/organization_memberships/${encodePathParameter(organizationMembershipId)}/groups`; return new AutoPaginatable( await fetchAndDeserialize( @@ -1264,7 +1270,7 @@ export class UserManagement { async getInvitation(invitationId: string): Promise { const { data } = await this.workos.get( - `/user_management/invitations/${invitationId}`, + `/user_management/invitations/${encodePathParameter(invitationId)}`, ); return deserializeInvitation(data); @@ -1279,7 +1285,7 @@ export class UserManagement { */ async findInvitationByToken(invitationToken: string): Promise { const { data } = await this.workos.get( - `/user_management/invitations/by_token/${invitationToken}`, + `/user_management/invitations/by_token/${encodePathParameter(invitationToken)}`, ); return deserializeInvitation(data); @@ -1348,7 +1354,7 @@ export class UserManagement { */ async acceptInvitation(invitationId: string): Promise { const { data } = await this.workos.post( - `/user_management/invitations/${invitationId}/accept`, + `/user_management/invitations/${encodePathParameter(invitationId)}/accept`, null, ); @@ -1364,7 +1370,7 @@ export class UserManagement { */ async revokeInvitation(invitationId: string): Promise { const { data } = await this.workos.post( - `/user_management/invitations/${invitationId}/revoke`, + `/user_management/invitations/${encodePathParameter(invitationId)}/revoke`, null, ); @@ -1389,7 +1395,7 @@ export class UserManagement { InvitationResponse, SerializedResendInvitationOptions >( - `/user_management/invitations/${invitationId}/resend`, + `/user_management/invitations/${encodePathParameter(invitationId)}/resend`, options ? serializeResendInvitationOptions(options) : {}, );