diff --git a/.changeset/three-badgers-hunt.md b/.changeset/three-badgers-hunt.md new file mode 100644 index 00000000000..08f26b67bd5 --- /dev/null +++ b/.changeset/three-badgers-hunt.md @@ -0,0 +1,15 @@ +--- +'@clerk/backend': patch +--- + +Adds the ability to create and revoke actor tokens to the Backend API client. + + +```ts + import { createClerkClient } from '@clerk/backend'; + + const clerkClient = createClerkClient(...); + + const { id } = await clerkClient.actorTokens.create({...}); + await clerkClient.actorTokens.revoke(id); +``` \ No newline at end of file diff --git a/packages/backend/src/api/endpoints/ActorTokenApi.ts b/packages/backend/src/api/endpoints/ActorTokenApi.ts new file mode 100644 index 00000000000..c3dca6a9706 --- /dev/null +++ b/packages/backend/src/api/endpoints/ActorTokenApi.ts @@ -0,0 +1,62 @@ +import { joinPaths } from '../../util/path'; +import type { ActorToken } from '../resources/ActorToken'; +import { AbstractAPI } from './AbstractApi'; + +const basePath = '/actor_tokens'; + +type ActorTokenActorCreateParams = { + /** + * The ID of the actor. + */ + sub: string; + /** + * Additional properties of the actor. + */ + additionalProperties?: { [k: string]: any }; +}; + +type ActorTokenCreateParams = { + /** + * The ID of the user being impersonated. + */ + userId: string; + /** + * The actor payload. It needs to include a sub property which should contain the ID of the actor. + * + * @remarks + * This whole payload will be also included in the JWT session token. + */ + actor: ActorTokenActorCreateParams; + /** + * Optional parameter to specify the life duration of the actor token in seconds. + * + * @remarks + * By default, the duration is 1 hour. + */ + expiresInSeconds?: number | undefined; + /** + * The maximum duration that the session which will be created by the generated actor token should last. + * + * @remarks + * By default, the duration of a session created via an actor token, lasts 30 minutes. + */ + sessionMaxDurationInSeconds?: number | undefined; +}; + +export class ActorTokenAPI extends AbstractAPI { + public async create(params: ActorTokenCreateParams) { + return this.request({ + method: 'POST', + path: basePath, + bodyParams: params, + }); + } + + public async revoke(actorTokenId: string) { + this.requireId(actorTokenId); + return this.request({ + method: 'POST', + path: joinPaths(basePath, actorTokenId, 'revoke'), + }); + } +} diff --git a/packages/backend/src/api/endpoints/index.ts b/packages/backend/src/api/endpoints/index.ts index f25350020e8..d7249afc0ab 100644 --- a/packages/backend/src/api/endpoints/index.ts +++ b/packages/backend/src/api/endpoints/index.ts @@ -1,3 +1,4 @@ +export * from './ActorTokenApi'; export * from './AccountlessApplicationsAPI'; export * from './AbstractApi'; export * from './AllowlistIdentifierApi'; diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index f90684666a9..d208152275f 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -1,5 +1,6 @@ import { AccountlessApplicationAPI, + ActorTokenAPI, AllowlistIdentifierAPI, BlocklistIdentifierAPI, ClientAPI, @@ -32,6 +33,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { __experimental_accountlessApplications: new AccountlessApplicationAPI( buildRequest({ ...options, requireSecretKey: false }), ), + actorTokens: new ActorTokenAPI(request), allowlistIdentifiers: new AllowlistIdentifierAPI(request), blocklistIdentifiers: new BlocklistIdentifierAPI(request), clients: new ClientAPI(request), diff --git a/packages/backend/src/api/resources/ActorToken.ts b/packages/backend/src/api/resources/ActorToken.ts new file mode 100644 index 00000000000..c9fca99cace --- /dev/null +++ b/packages/backend/src/api/resources/ActorToken.ts @@ -0,0 +1,28 @@ +import type { ActorTokenStatus } from './Enums'; +import type { ActorTokenJSON } from './JSON'; + +export class ActorToken { + constructor( + readonly id: string, + readonly status: ActorTokenStatus, + readonly userId: string, + readonly actor: Record | null, + readonly token: string | null | undefined, + readonly url: string | null | undefined, + readonly createdAt: number, + readonly updatedAt: number, + ) {} + + static fromJSON(data: ActorTokenJSON): ActorToken { + return new ActorToken( + data.id, + data.status, + data.user_id, + data.actor, + data.token, + data.url, + data.created_at, + data.updated_at, + ); + } +} diff --git a/packages/backend/src/api/resources/Deserializer.ts b/packages/backend/src/api/resources/Deserializer.ts index 77182f9964a..6f6f6842af8 100644 --- a/packages/backend/src/api/resources/Deserializer.ts +++ b/packages/backend/src/api/resources/Deserializer.ts @@ -1,4 +1,5 @@ import { + ActorToken, AllowlistIdentifier, BlocklistIdentifier, Client, @@ -73,6 +74,8 @@ function jsonToObject(item: any): any { switch (item.object) { case ObjectType.AccountlessApplication: return AccountlessApplication.fromJSON(item); + case ObjectType.ActorToken: + return ActorToken.fromJSON(item); case ObjectType.AllowlistIdentifier: return AllowlistIdentifier.fromJSON(item); case ObjectType.BlocklistIdentifier: diff --git a/packages/backend/src/api/resources/Enums.ts b/packages/backend/src/api/resources/Enums.ts index c4ffdb1c51b..9f9798af19c 100644 --- a/packages/backend/src/api/resources/Enums.ts +++ b/packages/backend/src/api/resources/Enums.ts @@ -37,6 +37,13 @@ export type SignUpStatus = 'missing_requirements' | 'complete' | 'abandoned'; export type InvitationStatus = 'pending' | 'accepted' | 'revoked' | 'expired'; +export const ActorTokenStatus = { + Pending: 'pending', + Accepted: 'accepted', + Revoked: 'revoked', +} as const; +export type ActorTokenStatus = (typeof ActorTokenStatus)[keyof typeof ActorTokenStatus]; + export type AllowlistIdentifierType = 'email_address' | 'phone_number' | 'web3_wallet'; export type BlocklistIdentifierType = AllowlistIdentifierType; diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index e53c7b3a96e..d0ce6e1b867 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -1,4 +1,5 @@ import type { + ActorTokenStatus, AllowlistIdentifierType, BlocklistIdentifierType, InvitationStatus, @@ -13,6 +14,7 @@ import type { export const ObjectType = { AccountlessApplication: 'accountless_application', + ActorToken: 'actor_token', AllowlistIdentifier: 'allowlist_identifier', BlocklistIdentifier: 'blocklist_identifier', Client: 'client', @@ -74,6 +76,18 @@ export interface AccountlessApplicationJSON extends ClerkResourceJSON { api_keys_url: string; } +export interface ActorTokenJSON extends ClerkResourceJSON { + object: typeof ObjectType.ActorToken; + id: string; + status: ActorTokenStatus; + user_id: string; + actor: Record | null; + token?: string | null; + url?: string | null; + created_at: number; + updated_at: number; +} + export interface AllowlistIdentifierJSON extends ClerkResourceJSON { object: typeof ObjectType.AllowlistIdentifier; identifier: string; diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index 945dfcd0841..798ee351b98 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -1,3 +1,4 @@ +export * from './ActorToken'; export * from './AccountlessApplication'; export * from './AllowlistIdentifier'; export * from './BlocklistIdentifier'; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9188825798d..198d398f951 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -54,6 +54,7 @@ export type { VerifyTokenOptions } from './tokens/verify'; * JSON types */ export type { + ActorTokenJSON, AccountlessApplicationJSON, ClerkResourceJSON, TokenJSON, @@ -98,6 +99,7 @@ export type { * Resources */ export type { + ActorToken, AccountlessApplication, AllowlistIdentifier, BlocklistIdentifier,