diff --git a/docs/.gitignore b/docs/.gitignore index 876092f4c4..2c04972d2e 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -31,5 +31,6 @@ next-env.d.ts /content/api/ /content/dashboard/ /public/openapi/ +/public/sdk-docs/ /openapi/ diff --git a/docs/code-examples/index.ts b/docs/code-examples/index.ts index 2aaa8f4bfd..986bffab6c 100644 --- a/docs/code-examples/index.ts +++ b/docs/code-examples/index.ts @@ -3,6 +3,7 @@ import { apiKeysExamples } from './api-keys'; import { conceptsExamples } from './concepts'; import { customizationExamples } from './customization'; import { paymentsExamples } from './payments'; +import { sdkEmailsExamples } from './sdk-emails'; import { selfHostExamples } from './self-host'; import { setupExamples } from './setup'; import { viteExamples } from './vite-example'; @@ -12,6 +13,9 @@ const allExamples: Record>> 'apps': {...apiKeysExamples, ...paymentsExamples }, 'concepts': conceptsExamples, 'getting-started': viteExamples, + 'sdk': { + ...sdkEmailsExamples, + }, 'others': selfHostExamples, 'customization': customizationExamples, }; diff --git a/docs/code-examples/sdk-emails.ts b/docs/code-examples/sdk-emails.ts new file mode 100644 index 0000000000..633f221986 --- /dev/null +++ b/docs/code-examples/sdk-emails.ts @@ -0,0 +1,168 @@ + +export const sdkEmailsExamples = { + 'types/email': { + 'send-html-email': [ + { + language: 'JavaScript', + framework: 'Next.js', + variant: 'client' as const, + code: `// ⚠️ Email sending is not available on the client side +// +// The sendEmail() method requires SECRET_SERVER_KEY and can only +// be used from server-side code (Server Components, API routes, etc.) +// +// To send emails from a client component, create an API route that +// calls stackServerApp.sendEmail() and call it from your client code. + +// Example: Call a server API route from client +async function sendEmailFromClient() { + const response = await fetch('/api/send-email', { + method: 'POST', + body: JSON.stringify({ + userIds: ['user-1', 'user-2'], + subject: 'Welcome!', + html: '

Welcome!

' + }) + }); + + return response.json(); +}`, + highlightLanguage: 'typescript', + filename: 'app/components/send-email-button.tsx' + }, + { + language: 'JavaScript', + framework: 'Next.js', + variant: 'server' as const, + code: `import { stackServerApp } from "@/stack"; + +export default async function SendWelcomeEmail() { + const result = await stackServerApp.sendEmail({ + userIds: ['user-1', 'user-2'], + subject: 'Welcome to our platform!', + html: '

Welcome!

Thanks for joining us.

', + }); + + if (result.status === 'error') { + console.error('Failed to send email:', result.error); + } + + return
Email sent!
; +}`, + highlightLanguage: 'typescript', + filename: 'app/api/send-email/route.ts' + }, + { + language: 'Python', + framework: 'Flask', + code: `import requests + +def send_welcome_email(): + response = requests.post( + 'https://api.stack-auth.com/api/v1/emails/send', + headers={ + 'x-stack-access-type': 'server', + 'x-stack-project-id': stack_project_id, + 'x-stack-secret-server-key': stack_secret_server_key, + }, + json={ + 'user_ids': ['user-1', 'user-2'], + 'subject': 'Welcome to our platform!', + 'html': '

Welcome!

Thanks for joining us.

', + } + ) + + return response.json()`, + highlightLanguage: 'python', + filename: 'send_email.py' + } + ], + 'send-template-email': [ + { + language: 'JavaScript', + framework: 'Next.js', + variant: 'client' as const, + code: `// ⚠️ Email sending is not available on the client side +// +// The sendEmail() method requires SECRET_SERVER_KEY and can only +// be used from server-side code (Server Components, API routes, etc.) +// +// To send emails from a client component, create an API route that +// calls stackServerApp.sendEmail() and call it from your client code. + +// Example: Call a server API route from client +async function sendTemplateEmailFromClient() { + const response = await fetch('/api/send-template-email', { + method: 'POST', + body: JSON.stringify({ + userId: 'user-1', + templateId: 'welcome-template', + variables: { userName: 'John Doe' } + }) + }); + + return response.json(); +}`, + highlightLanguage: 'typescript', + filename: 'app/components/send-email-button.tsx' + }, + { + language: 'JavaScript', + framework: 'Next.js', + variant: 'server' as const, + code: `import { stackServerApp } from "@/stack"; + +export default async function SendTemplateEmail() { + const result = await stackServerApp.sendEmail({ + userIds: ['user-1'], + templateId: 'welcome-template', + variables: { + userName: 'John Doe', + activationUrl: 'https://app.com/activate/token123' + }, + subject: 'Welcome aboard!', + notificationCategoryName: 'product_updates' + }); + + if (result.status === 'error') { + console.error('Failed to send email:', result.error); + } + + return
Template email sent!
; +}`, + highlightLanguage: 'typescript', + filename: 'app/api/send-template-email/route.ts' + }, + { + language: 'Python', + framework: 'Flask', + code: `import requests + +def send_template_email(): + response = requests.post( + 'https://api.stack-auth.com/api/v1/emails/send', + headers={ + 'x-stack-access-type': 'server', + 'x-stack-project-id': stack_project_id, + 'x-stack-secret-server-key': stack_secret_server_key, + }, + json={ + 'user_ids': ['user-1'], + 'template_id': 'welcome-template', + 'variables': { + 'userName': 'John Doe', + 'activationUrl': 'https://app.com/activate/token123' + }, + 'subject': 'Welcome aboard!', + 'notification_category_name': 'product_updates' + } + ) + + return response.json()`, + highlightLanguage: 'python', + filename: 'send_template_email.py' + } + ] + } +}; + diff --git a/docs/content/docs/sdk/hooks/use-stack-app.mdx b/docs/content/docs/sdk/hooks/use-stack-app.mdx index 3e55c03fb6..f7500cc508 100644 --- a/docs/content/docs/sdk/hooks/use-stack-app.mdx +++ b/docs/content/docs/sdk/hooks/use-stack-app.mdx @@ -4,7 +4,7 @@ title: useStackApp The `useStackApp` hook returns a `StackClientApp` object from the one that you provided in the [`StackProvider` component](../../components/stack-provider.mdx). If you want to learn more about the `StackClientApp` object, check out the [StackApp](../objects/stack-app.mdx) documentation. -Example: +## Usage Example ```jsx import { useStackApp } from "@stackframe/stack"; @@ -14,3 +14,7 @@ function MyComponent() { return
Sign In URL: {stackApp.urls.signIn}
; } ``` + +## API Reference + + diff --git a/docs/content/docs/sdk/hooks/use-user.mdx b/docs/content/docs/sdk/hooks/use-user.mdx index a094b20d42..3b14e60995 100644 --- a/docs/content/docs/sdk/hooks/use-user.mdx +++ b/docs/content/docs/sdk/hooks/use-user.mdx @@ -5,3 +5,7 @@ title: useUser This standalone React hook is an alias for `useStackApp().useUser()`. It only exists for convenience; it does not have any additional functionality. For more information, please refer to the [documentation for `stackClientApp.useUser()`](../objects/stack-app.mdx#stackclientappuseuseroptions). + +## API Reference + + diff --git a/docs/content/docs/sdk/index.mdx b/docs/content/docs/sdk/index.mdx index eb4e08f43f..b909b795c9 100644 --- a/docs/content/docs/sdk/index.mdx +++ b/docs/content/docs/sdk/index.mdx @@ -17,7 +17,7 @@ export const sdkSections = [ ] }, { - title: "Users & user data", + title: "Users & User Data", items: [ { name: "CurrentUser", href: "types/user#currentuser", icon: "type" }, { name: "ServerUser", href: "types/user#serveruser", icon: "type" }, @@ -26,6 +26,14 @@ export const sdkSections = [ { name: "ServerContactChannel", href: "types/contact-channel#servercontactchannel", icon: "type" }, ] }, + { + title: "Authentication", + items: [ + { name: "OAuthProvider", href: "types/connected-account#oauthprovider", icon: "type" }, + { name: "OAuthConnection", href: "types/connected-account#oauthconnection", icon: "type" }, + { name: "ActiveSession", href: "types/connected-account#activesession", icon: "type" }, + ] + }, { title: "Teams", items: [ @@ -39,6 +47,15 @@ export const sdkSections = [ { name: "ServerTeamProfile", href: "types/team-profile#serverteamprofile", icon: "type" }, ] }, + { + title: "API Keys", + items: [ + { name: "UserApiKey", href: "types/api-key#userapikey", icon: "type" }, + { name: "UserApiKeyFirstView", href: "types/api-key#userapikeyfirstview", icon: "type" }, + { name: "TeamApiKey", href: "types/api-key#teamapikey", icon: "type" }, + { name: "TeamApiKeyFirstView", href: "types/api-key#teamapikeyfirstview", icon: "type" }, + ] + }, { title: "Email", items: [ diff --git a/docs/content/docs/sdk/meta.json b/docs/content/docs/sdk/meta.json index 1b4ebdc314..79a102ecea 100644 --- a/docs/content/docs/sdk/meta.json +++ b/docs/content/docs/sdk/meta.json @@ -11,15 +11,19 @@ "types/user", "types/team", "types/team-user", - "types/team-permission", "types/team-profile", "types/contact-channel", - "types/email", + "types/team-permission", "types/api-key", + "types/item", "types/project", "types/connected-account", - "types/item", - "types/customer", + "---Mixins---", + "mixins/customer", + "mixins/auth", + "mixins/connection", + "---Shared Types---", + "types/email", "---Hooks---", "hooks/use-stack-app", "hooks/use-user" diff --git a/docs/content/docs/sdk/mixins/auth.mdx b/docs/content/docs/sdk/mixins/auth.mdx new file mode 100644 index 0000000000..8126ee53b4 --- /dev/null +++ b/docs/content/docs/sdk/mixins/auth.mdx @@ -0,0 +1,11 @@ +--- +title: Auth +full: true +--- + +The `Auth` mixin provides session and authentication functionality. It is included in `CurrentUser`. + +# `Auth` + + + diff --git a/docs/content/docs/sdk/mixins/connection.mdx b/docs/content/docs/sdk/mixins/connection.mdx new file mode 100644 index 0000000000..768f7ea0df --- /dev/null +++ b/docs/content/docs/sdk/mixins/connection.mdx @@ -0,0 +1,11 @@ +--- +title: Connection +full: true +--- + +The `Connection` mixin provides the base interface for OAuth connections. It is included in `OAuthConnection`. + +# `Connection` + + + diff --git a/docs/content/docs/sdk/mixins/customer.mdx b/docs/content/docs/sdk/mixins/customer.mdx new file mode 100644 index 0000000000..b9db0e4421 --- /dev/null +++ b/docs/content/docs/sdk/mixins/customer.mdx @@ -0,0 +1,11 @@ +--- +title: Customer +full: true +--- + +The `Customer` mixin provides payment and billing functionality. It is included in both `CurrentUser` and `Team` types. + +# `Customer` + + + diff --git a/docs/content/docs/sdk/objects/stack-app.mdx b/docs/content/docs/sdk/objects/stack-app.mdx index 7b2e9c10db..6ffa424d5f 100644 --- a/docs/content/docs/sdk/objects/stack-app.mdx +++ b/docs/content/docs/sdk/objects/stack-app.mdx @@ -34,8 +34,23 @@ Most commonly you get an instance of `StackClientApp` by calling [`useStackApp() signInWithOAuth(provider): void; //$stack-link-to:#stackclientappsigninwithoauthprovider signInWithCredential([options]): Promise<...>; //$stack-link-to:#stackclientappsigninwithcredentialoptions signUpWithCredential([options]): Promise<...>; //$stack-link-to:#stackclientappsignupwithcredentialoptions + signInWithMagicLink(code[, options]): Promise; //$stack-link-to:#stackclientappsigninwithmagiclinkcode + signInWithPasskey(): Promise; //$stack-link-to:#stackclientappsigninwithpasskey + signInWithMfa(totp, code[, options]): Promise; //$stack-link-to:#stackclientappsigninwithmfa + signOut([options]): Promise; //$stack-link-to:#stackclientappsignout + sendForgotPasswordEmail(email): Promise<...>; //$stack-link-to:#stackclientappsendforgotpasswordemailemail sendMagicLinkEmail(email): Promise<...>; //$stack-link-to:#stackclientappsendmagiclinkemailemail + resetPassword(options): Promise; //$stack-link-to:#stackclientappresetpassword + verifyPasswordResetCode(code): Promise; //$stack-link-to:#stackclientappverifypasswordresetcode + verifyEmail(code): Promise; //$stack-link-to:#stackclientappverifyemail + + acceptTeamInvitation(code): Promise; //$stack-link-to:#stackclientappacceptteaminvitation + verifyTeamInvitationCode(code): Promise; //$stack-link-to:#stackclientappverifyteaminvitationcode + getTeamInvitationDetails(code): Promise<...>; //$stack-link-to:#stackclientappgetteaminvitationdetails + + getAccessToken(): Promise; //$stack-link-to:#stackclientappgetaccesstoken + getAuthHeaders(): Promise<{ "x-stack-auth": string }>; //$stack-link-to:#stackclientappgetauthheaders };`} /> ## Constructor @@ -498,6 +513,443 @@ const result = await stackClientApp.sendMagicLinkEmail("test@example.com"); + + + + +Completes sign-in using a magic link code from the email. + +**Parameters:** +- `code` (string) - The code from the magic link URL +- `options?` (object) + - `noRedirect?` (boolean) - If true, don't redirect after sign-in + +**Returns:** `Promise` + + + + + + +```typescript +declare function signInWithMagicLink( + code: string, + options?: { noRedirect?: boolean } +): Promise; +``` + + + +```typescript +// In the magic link callback page +const code = new URLSearchParams(window.location.search).get("code"); +if (code) { + await stackClientApp.signInWithMagicLink(code); +} +``` + + + + + + + + + +Initiates passwordless sign-in using a passkey (WebAuthn). This opens a browser prompt for biometric or security key authentication. + +**Note:** This method is only available in browser environments that support WebAuthn. + +**Parameters:** None + +**Returns:** `Promise` + + + + + + +```typescript +declare function signInWithPasskey(): Promise; +``` + + + +```typescript +try { + await stackClientApp.signInWithPasskey(); + // User is now signed in +} catch (error) { + console.error("Passkey sign-in failed:", error); +} +``` + + + + + + + + + +Completes sign-in when MFA is required. Called after receiving a `MultiFactorAuthenticationRequired` error from another sign-in method. + +**Parameters:** +- `totp` (string) - The 6-digit TOTP code from the authenticator app +- `code` (string) - The attempt code from the MFA error or sessionStorage +- `options?` (object) + - `noRedirect?` (boolean) - If true, don't redirect after sign-in + +**Returns:** `Promise` + + + + + + +```typescript +declare function signInWithMfa( + totp: string, + code: string, + options?: { noRedirect?: boolean } +): Promise; +``` + + + +```typescript +// After sign-in returns MFA required error +await stackClientApp.signInWithMfa( + "123456", // TOTP code from authenticator + attemptCode // From the MFA error response +); +``` + + + + + + + + + +Signs out the current user by invalidating their session and clearing stored tokens. + +**Parameters:** +- `options?` (object) + - `redirectUrl?` (string) - Where to redirect after sign out. Defaults to the `afterSignOut` URL. + +**Returns:** `Promise` + + + + + + +```typescript +declare function signOut(options?: { + redirectUrl?: string +}): Promise; +``` + + + +```typescript +await stackClientApp.signOut(); +// or with custom redirect +await stackClientApp.signOut({ redirectUrl: "/goodbye" }); +``` + + + + + + + + + +Resets the user's password using a code from the password reset email. + +**Parameters:** +- `options` (object) + - `code` (string) - The code from the password reset email + - `password` (string) - The new password + +**Returns:** `Promise` + + + + + + +```typescript +declare function resetPassword(options: { + code: string; + password: string; +}): Promise; +``` + + + +```typescript +const code = new URLSearchParams(window.location.search).get("code"); +await stackClientApp.resetPassword({ + code, + password: "newSecurePassword123!", +}); +``` + + + + + + + + + +Verifies a password reset code is valid before showing the reset form. Call this before showing the password input to avoid user frustration. + +**Parameters:** +- `code` (string) - The code from the password reset email + +**Returns:** `Promise` + + + + + + +```typescript +declare function verifyPasswordResetCode(code: string): Promise; +``` + + + +```typescript +const code = new URLSearchParams(window.location.search).get("code"); +try { + await stackClientApp.verifyPasswordResetCode(code); + // Code is valid, show password reset form +} catch (error) { + // Code is invalid or expired +} +``` + + + + + + + + + +Verifies a user's email using the code from the verification email. + +**Parameters:** +- `code` (string) - The verification code from the email + +**Returns:** `Promise` + + + + + + +```typescript +declare function verifyEmail(code: string): Promise; +``` + + + +```typescript +const code = new URLSearchParams(window.location.search).get("code"); +if (code) { + await stackClientApp.verifyEmail(code); + console.log("Email verified successfully"); +} +``` + + + + + + + + + +Accepts a team invitation using the code from the invitation email. The user must be signed in to accept an invitation. + +**Parameters:** +- `code` (string) - The invitation code from the email + +**Returns:** `Promise` + + + + + + +```typescript +declare function acceptTeamInvitation(code: string): Promise; +``` + + + +```typescript +const code = new URLSearchParams(window.location.search).get("code"); +if (code) { + await stackClientApp.acceptTeamInvitation(code); + console.log("Joined team successfully"); +} +``` + + + + + + + + + +Verifies a team invitation code is valid before accepting. Useful for showing invitation details before the user confirms. + +**Parameters:** +- `code` (string) - The invitation code from the email + +**Returns:** `Promise` + + + + + + +```typescript +declare function verifyTeamInvitationCode(code: string): Promise; +``` + + + +```typescript +const code = new URLSearchParams(window.location.search).get("code"); +try { + await stackClientApp.verifyTeamInvitationCode(code); + // Code is valid, show confirmation dialog +} catch (error) { + // Code is invalid or expired +} +``` + + + + + + + + + +Gets details about a team invitation, such as the team name. + +**Parameters:** +- `code` (string) - The invitation code from the email + +**Returns:** `Promise<{ teamDisplayName: string }>` + + + + + + +```typescript +declare function getTeamInvitationDetails( + code: string +): Promise<{ teamDisplayName: string }>; +``` + + + +```typescript +const code = new URLSearchParams(window.location.search).get("code"); +const details = await stackClientApp.getTeamInvitationDetails(code); +console.log(`You've been invited to join ${details.teamDisplayName}`); +``` + + + + + + + + + +Gets the current access token, refreshing it if needed. + +**Parameters:** None + +**Returns:** `Promise` - The access token, or `null` if not authenticated + + + + + + +```typescript +declare function getAccessToken(): Promise; +``` + + + +```typescript +const token = await stackClientApp.getAccessToken(); +if (token) { + // Use token for API calls +} +``` + + + + + + + + + +Gets authentication headers for cross-origin requests. Use this when making requests to a different domain where cookies can't be sent. + +**Parameters:** None + +**Returns:** `Promise<{ "x-stack-auth": string }>` - Headers object with JSON-encoded tokens + + + + + + +```typescript +declare function getAuthHeaders(): Promise<{ "x-stack-auth": string }>; +``` + + + +```typescript +// Client-side +const headers = await stackClientApp.getAuthHeaders(); +const response = await fetch("https://api.example.com/data", { + headers: { + ...headers, + "Content-Type": "application/json", + }, +}); + +// Server-side (to read the tokens) +const user = await stackServerApp.getUser({ + tokenStore: request +}); +``` + + + + + --- # StackServerApp @@ -525,7 +977,6 @@ exposing [`SECRET_SERVER_KEY`](../../rest-api/overview.mdx) on the client. // NEXT_LINE_PLATFORM react-like ⤷ useUsers([options]): ServerUser[]; //$stack-link-to:#stackserverappuseusersoptions createUser([options]): Promise; //$stack-link-to:#stackserverappcreateuseroptions - sendEmail(options): Promise>; //$stack-link-to:#stackserverappsendemailoptions getTeam(id): Promise; //$stack-link-to:#stackserverappgetteamid // NEXT_LINE_PLATFORM react-like @@ -534,6 +985,11 @@ exposing [`SECRET_SERVER_KEY`](../../rest-api/overview.mdx) on the client. // NEXT_LINE_PLATFORM react-like ⤷ useTeams(): ServerTeam[]; //$stack-link-to:#stackserverappuseteams createTeam([options]): Promise; //$stack-link-to:#stackserverappcreateteamoptions + + sendEmail(options): Promise>; //$stack-link-to:#stackserverappsendemailoptions + getEmailDeliveryStats(): Promise; //$stack-link-to:#stackserverappgetemaildeliverystats + grantProduct(options): Promise; //$stack-link-to:#stackserverappgrantproduct + getDataVaultStore(id): DataVaultStore; //$stack-link-to:#stackserverappgetdatavaultstore }`} /> ## Constructor @@ -985,3 +1441,229 @@ const team = await stackServerApp.createTeam({ + +## Email & Notifications + + + + + + + Send custom emails to users. You can send either custom HTML emails or use predefined templates with variables. + + **Parameters:** + - `options` ([SendEmailOptions](../types/email#sendemailoptions)) - Email configuration and content + + **Returns:** `Promise>` + + The method returns a `Result` object that can contain specific error types: + + - `RequiresCustomEmailServer` - No custom email server configured + - `SchemaError` - Invalid email data provided + - `UserIdDoesNotExist` - One or more user IDs don't exist + + + + + + +```typescript +declare function sendEmail(options: SendEmailOptions): Promise>; +``` + + + + + + Send HTML Email + Send Template Email + + +```typescript +const result = await stackServerApp.sendEmail({ + userIds: ['user-1', 'user-2'], + subject: 'Welcome to our platform!', + html: '

Welcome!

Thanks for joining us.

', +}); + +if (result.status === 'error') { + console.error('Failed to send email:', result.error); +} +``` +
+ +```typescript +const result = await stackServerApp.sendEmail({ + userIds: ['user-1'], + templateId: 'welcome-template', + variables: { + userName: 'John Doe', + activationUrl: 'https://app.com/activate/token123', + }, +}); + +if (result.status === 'error') { + console.error('Failed to send email:', result.error); +} +``` + +
+ +
+ +
+
+
+ + + + + +Gets email delivery statistics for the project. + +**Parameters:** None + +**Returns:** `Promise` + +```typescript +type EmailDeliveryInfo = { + delivered: number; // Emails successfully delivered + bounced: number; // Emails that bounced + complained: number; // Emails marked as spam + total: number; // Total emails sent +}; +``` + + + + + + +```typescript +declare function getEmailDeliveryStats(): Promise; +``` + + + +```typescript +const stats = await stackServerApp.getEmailDeliveryStats(); +console.log(`Delivered: ${stats.delivered}/${stats.total}`); +console.log(`Bounce rate: ${(stats.bounced / stats.total * 100).toFixed(1)}%`); +``` + + + + + +## Payments & Products + + + + + +Grants a product to a customer (user, team, or custom customer ID) without going through checkout. + +**Parameters:** +- `options` (object) + - Customer identification (one of): + - `userId` (string) - Grant to a user + - `teamId` (string) - Grant to a team + - `customCustomerId` (string) - Grant to a custom customer + - Product identification (one of): + - `productId` (string) - Existing product ID + - `product` (object) - Inline product definition + - `quantity?` (number) - Quantity to grant (default: 1) + +**Returns:** `Promise` + + + + + + +```typescript +declare function grantProduct(options: { + userId?: string; + teamId?: string; + customCustomerId?: string; + productId?: string; + product?: { name: string; description?: string; /* ... */ }; + quantity?: number; +}): Promise; +``` + + + +```typescript +// Grant premium access to a user +await stackServerApp.grantProduct({ + userId: "user_123", + productId: "prod_premium", + quantity: 1, +}); + +// Grant credits to a team +await stackServerApp.grantProduct({ + teamId: "team_456", + productId: "prod_credits", + quantity: 100, +}); +``` + + + + + +## Data Vault + + + + + +Gets a Data Vault store for storing sensitive key-value data server-side. Each store is isolated and identified by its ID. + +**Parameters:** +- `id` (string) - The data vault store ID + +**Returns:** `DataVaultStore` - An object with methods to manage key-value pairs + +```typescript +type DataVaultStore = { + id: string; + get(key: string): Promise; + set(key: string, value: string): Promise; + delete(key: string): Promise; + list(): Promise; +}; +``` + + + + + + +```typescript +declare function getDataVaultStore(id: string): DataVaultStore; +``` + + + +```typescript +const vault = stackServerApp.getDataVaultStore("user-secrets"); + +// Store a value +await vault.set("api-key-user123", "sk_live_abc123"); + +// Retrieve a value +const apiKey = await vault.get("api-key-user123"); + +// List all keys +const keys = await vault.list(); + +// Delete a value +await vault.delete("api-key-user123"); +``` + + + + diff --git a/docs/content/docs/sdk/types/api-key.mdx b/docs/content/docs/sdk/types/api-key.mdx index f0ee2ec491..1d3a965406 100644 --- a/docs/content/docs/sdk/types/api-key.mdx +++ b/docs/content/docs/sdk/types/api-key.mdx @@ -3,433 +3,56 @@ title: ApiKey full: true --- -`ApiKey` represents an authentication token that allows programmatic access to your application's backend. API keys can be associated with individual users or teams. +API keys provide programmatic access to your application's backend. They can be created for individual users or teams. On this page: -- [`ApiKey`](#apikey) -- Types: - - [`UserApiKey`](#userapikey) - - [`TeamApiKey`](#teamapikey) +- [UserApiKey](#userapikey) +- [UserApiKeyFirstView](#userapikeyfirstview) +- [TeamApiKey](#teamapikey) +- [TeamApiKeyFirstView](#teamapikeyfirstview) --- -# `ApiKey` +## Accessing API Keys -API keys provide a way for users to authenticate with your backend services without using their primary credentials. They can be created for individual users or for teams, allowing programmatic access to your application. +**For Users:** +- [`user.createApiKey(options)`](../types/user.mdx#currentusercreateapikey) - Returns `UserApiKeyFirstView` (includes full `value`) +- [`user.listApiKeys()`](../types/user.mdx#currentuserlistapikeys) - Returns `UserApiKey[]` +- [`user.useApiKeys()`](../types/user.mdx#currentuseruseapikeys) - React hook returning `UserApiKey[]` -API keys can be obtained through: -- [`user.createApiKey()`](../types/user.mdx#currentusercreateapikeyoptions) -- [`user.listApiKeys()`](../types/user.mdx#currentuserlistapikeys) -- [`user.useApiKeys()`](../types/user.mdx#currentuseruseapikeys) (React hook) -- [`team.createApiKey()`](../types/team.mdx#teamcreateapikeyoptions) -- [`team.listApiKeys()`](../types/team.mdx#teamlistapikeys) -- [`team.useApiKeys()`](../types/team.mdx#teamuseapikeys) (React hook) - -### Table of Contents - - = { - id: string; //$stack-link-to:#apikeyid - description: string; //$stack-link-to:#apikeydescription - expiresAt?: Date; //$stack-link-to:#apikeyexpiresat - manuallyRevokedAt: Date | null; //$stack-link-to:#apikeymanuallyrevokedat - createdAt: Date; //$stack-link-to:#apikeycreatedat - value: IsFirstView extends true ? string : { lastFour: string }; //$stack-link-to:#apikeyvalue - - // User or Team properties based on Type - ...(Type extends "user" ? { - type: "user"; - userId: string; //$stack-link-to:#apikeyuserid - } : { - type: "team"; - teamId: string; //$stack-link-to:#apikeyteamid - }) - - // Methods - isValid(): boolean; //$stack-link-to:#apikeyisvalid - whyInvalid(): "manually-revoked" | "expired" | null; //$stack-link-to:#apikeywhyinvalid - revoke(): Promise; //$stack-link-to:#apikeyrevoke - update(options): Promise; //$stack-link-to:#apikeyupdateoptions -};`} /> +**For Teams:** +- [`team.createApiKey(options)`](../types/team.mdx#teamcreateapikey) - Returns `TeamApiKeyFirstView` (includes full `value`) +- [`team.listApiKeys()`](../types/team.mdx#teamlistapikeys) - Returns `TeamApiKey[]` +- [`team.useApiKeys()`](../types/team.mdx#teamuseapikeys) - React hook returning `TeamApiKey[]` --- - - - - The unique identifier for this API key. - - - - ```typescript - declare const id: string; - ``` - - - - - - - - A human-readable description of the API key's purpose. - - - - ```typescript - declare const description: string; - ``` - - - - - - - - The date and time when this API key will expire. If not set, the key does not expire. - - - - ```typescript - declare const expiresAt?: Date; - ``` - - - - - - - - The date and time when this API key was manually revoked. If null, the key has not been revoked. - - - - ```typescript - declare const manuallyRevokedAt: Date | null; - ``` - - - - - - - - The date and time when this API key was created. - - - - ```typescript - declare const createdAt: Date; - ``` - - - - - - - - The value of the API key. When the key is first created, this is the full API key string. After that, only the last four characters are available for security reasons. - - - - ```typescript - // On first creation - declare const value: string; - - // On subsequent retrievals - declare const value: { lastFour: string }; - ``` - - - - - - - - For user API keys, the ID of the user that owns this API key. - - - - ```typescript - declare const userId: string; - ``` - - - - - - - - For team API keys, the ID of the team that owns this API key. - - - - ```typescript - declare const teamId: string; - ``` - - - - - - - - Checks if the API key is still valid (not expired and not revoked). - - ### Parameters - - None. - - ### Returns - - `boolean`: True if the key is valid, false otherwise. - - - - - ```typescript - declare function isValid(): boolean; - ``` - - - ```typescript Checking if an API key is valid - if (apiKey.isValid()) { - console.log("API key is still valid"); - } else { - console.log("API key is invalid"); - } - ``` - - - - +# `UserApiKey` - - - - Returns the reason why the API key is invalid, or null if it is valid. +User API keys with masked value (returned by `listApiKeys()`/`useApiKeys()`). The full key value is only visible when first created. - ### Parameters - - None. - - ### Returns - - `"manually-revoked" | "expired" | null`: The reason the key is invalid, or null if it's valid. - - - - - ```typescript - declare function whyInvalid(): "manually-revoked" | "expired" | null; - ``` - - - ```typescript Checking why an API key is invalid - const reason = apiKey.whyInvalid(); - if (reason) { - console.log(`API key is invalid because it was ${reason}`); - } else { - console.log("API key is valid"); - } - ``` - - - - - - - - - Revokes the API key, preventing it from being used for authentication. - - ### Parameters - - None. - - ### Returns - - `Promise` - - - - - ```typescript - declare function revoke(): Promise; - ``` - - - ```typescript Revoking an API key - await apiKey.revoke(); - console.log("API key has been revoked"); - ``` - - - - - - - - - Updates the API key properties. - - ### Parameters - - - An object containing properties for updating. - - - A new description for the API key. - - - A new expiration date, or null to remove the expiration. - - - Set to true to revoke the API key. - - - - - ### Returns - - `Promise` - - - - - ```typescript - declare function update(options: { - description?: string; - expiresAt?: Date | null; - revoked?: boolean; - }): Promise; - ``` - - - ```typescript Updating an API key - await apiKey.update({ - description: "Updated description", - expiresAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), // 60 days - }); - ``` - - - - + --- -# Types - - - - - A type alias for an API key owned by a user. - - - - ```typescript - type UserApiKey = ApiKey<"user", false>; - ``` - - - +# `UserApiKeyFirstView` - - - - A type alias for a newly created user API key, which includes the full key value instead of just the last four characters. - - +User API key with full value visible (returned by `createApiKey()`). This is the same as `UserApiKey` but the `value` property contains the full key string instead of just the last four characters. - ```typescript - type UserApiKeyFirstView = ApiKey<"user", true>; - ``` - - - - - - - - A type alias for an API key owned by a team. - - - - ```typescript - type TeamApiKey = ApiKey<"team", false>; - ``` - - - - - - - - A type alias for a newly created team API key, which includes the full key value instead of just the last four characters. - - - - ```typescript - type TeamApiKeyFirstView = ApiKey<"team", true>; - ``` - - - + --- -# Creation Options - -When creating an API key using [`user.createApiKey()`](../types/user.mdx#currentusercreatekeyoptions) or [`team.createApiKey()`](../types/team.mdx#teamcreatekeyoptions), you need to provide an options object. +# `TeamApiKey` - - - The options object for creating an API key. +Team API keys with masked value (returned by `listApiKeys()`/`useApiKeys()`). - ### Properties + - - A human-readable description of the API key's purpose. - - - The date when the API key will expire. Use null for keys that don't expire. - - - Whether the API key is public. Defaults to false. - - - **Secret API Keys** (default) are monitored by Stack Auth's secret scanner, which can revoke them if detected in public code repositories. - - **Public API Keys** are designed for client-side code where exposure is not a concern. - - - - +--- - ```typescript - type ApiKeyCreationOptions = { - description: string; - expiresAt: Date | null; - isPublic?: boolean; - }; - ``` - - - ```typescript Creating a user API key - // Get the current user - const user = await stackApp.getUser(); +# `TeamApiKeyFirstView` - // Create a secret API key (default) - const secretKey = await user.createApiKey({ - description: "Backend integration", - expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days - isPublic: false, - }); +Team API key with full value visible (returned by `createApiKey()`). - // Create a public API key - const publicKey = await user.createApiKey({ - description: "Client-side access", - expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days - isPublic: true, - }); - ``` - - - + diff --git a/docs/content/docs/sdk/types/connected-account.mdx b/docs/content/docs/sdk/types/connected-account.mdx index 1b52d4fcf1..e5890c3314 100644 --- a/docs/content/docs/sdk/types/connected-account.mdx +++ b/docs/content/docs/sdk/types/connected-account.mdx @@ -1,200 +1,14 @@ --- -title: ConnectedAccount +title: Connected Account full: true --- -`OAuthConnection` represents an OAuth connection to an external provider (like Google, GitHub, etc.) that is linked to a user. You can use connected accounts to access the user's data on those platforms, such as reading Google Drive files or sending emails via Gmail. +Connected accounts represent OAuth connections to external providers (like Google, GitHub, etc.) that are linked to a user. You can use them to access the user's data on those platforms. -For a guide on how to use connected accounts, see the [OAuth guide](../../apps/oauth). - -On this page: -- [`Connection`](#connection) -- [`OAuthConnection`](#oauthconnection) - -# `Connection` - -Basic information about a connected account. This is the base type that `OAuthConnection` extends. - -### Table of Contents - - - - - - - The provider config ID. This is the same as `provider` and exists for backward compatibility. - - - - ```typescript - declare const id: string; - ``` - - - - - - - - The provider config ID (e.g., `"google"`, `"github"`). - - - - ```typescript - declare const provider: string; - ``` - - - - - - - - The account ID from the OAuth provider (e.g., the Google user ID). - - - - ```typescript - declare const providerAccountId: string; - ``` - - - - ---- +For a guide on how to use connected accounts, see [OAuth](/docs/concepts/oauth). # `OAuthConnection` -Extends `Connection` with methods to retrieve OAuth access tokens. Get it with: -- [`user.getConnectedAccount({ provider, providerAccountId })`](../types/user.mdx#currentusergetconnectedaccount) -- [`user.useConnectedAccount({ provider, providerAccountId })`](../types/user.mdx#currentuseruseconnectedaccount) {/* THIS_LINE_PLATFORM react-like */} -- [`user.listConnectedAccounts()`](../types/user.mdx#currentuserlistconnectedaccounts) -- [`user.useConnectedAccounts()`](../types/user.mdx#currentuseruseconnectedaccounts) {/* THIS_LINE_PLATFORM react-like */} -- [`user.getOrLinkConnectedAccount(provider)`](../types/user.mdx#currentusergetorlinkconnectedaccount) -- [`user.useOrLinkConnectedAccount(provider)`](../types/user.mdx#currentuseruseorlinkconnectedaccount) {/* THIS_LINE_PLATFORM react-like */} - -### Table of Contents - -; //$stack-link-to:#oauthconnectiongetaccesstokenoptions - useAccessToken(options?): Result; //$stack-link-to:#oauthconnectionuseaccesstokenoptions - };`} /> - ---- - - - - - Gets an OAuth access token for this connected account. The token can be used to call the provider's APIs on the user's behalf. - - Returns a `Result` object: - - On success: `{ status: "ok", data: { accessToken: string } }` - - On error: `{ status: "error", error: OAuthAccessTokenNotAvailable }` if the refresh token has been revoked/expired or the requested scopes are not available. - - ### Parameters - - - - - If provided, only returns a token that has all of these scopes. If the current token doesn't have the required scopes, the result will be an error. - - - - - ### Returns - - `Promise>` - - - - - ```typescript - declare function getAccessToken(options?: { - scopes?: string[]; - }): Promise>; - ``` - - - ```typescript Getting an access token - const result = await account.getAccessToken(); - if (result.status === "ok") { - const { accessToken } = result.data; - // Use accessToken to call provider APIs - } - ``` - ```typescript With scopes - const result = await account.getAccessToken({ - scopes: ["https://www.googleapis.com/auth/drive.readonly"], - }); - ``` - - - - - - - {/* THIS_LINE_PLATFORM react-like */} - - - React hook version of `getAccessToken`. Returns the access token result reactively. - - Returns a `Result` object: - - On success: `{ status: "ok", data: { accessToken: string } }` - - On error: `{ status: "error", error: OAuthAccessTokenNotAvailable }` if the refresh token has been revoked/expired or the requested scopes are not available. - - ### Parameters - - - - - If provided, only returns a token that has all of these scopes. - - - - - ### Returns - - `Result<{ accessToken: string }, OAuthAccessTokenNotAvailable>` - - - - - ```typescript - declare function useAccessToken(options?: { - scopes?: string[]; - }): Result< - { accessToken: string }, - OAuthAccessTokenNotAvailable - >; - ``` - - - ```tsx Using the access token hook - function MyComponent() { - const user = useUser({ or: "redirect" }); - const accounts = user.useConnectedAccounts(); - const googleAccount = accounts.find( - a => a.provider === "google" - ); - const result = googleAccount?.useAccessToken(); +Represents a connected OAuth account. Get it with `user.getConnectedAccount(providerId)` or `user.useConnectedAccount(providerId)`. - if (result?.status === "ok") { - return
Token: {result.data.accessToken}
; - } - return
No Google token available
; - } - ``` -
-
-
-
+ diff --git a/docs/content/docs/sdk/types/contact-channel.mdx b/docs/content/docs/sdk/types/contact-channel.mdx index 7704da60fb..1c6e096cef 100644 --- a/docs/content/docs/sdk/types/contact-channel.mdx +++ b/docs/content/docs/sdk/types/contact-channel.mdx @@ -18,215 +18,9 @@ Usually obtained by calling [`user.listContactChannels()`](../types/user.mdx#cur or [`user.useContactChannels()`](../types/user.mdx#currentuserusecontactchannels) {/* THIS_LINE_PLATFORM react-like */} . -### Table of Contents + -; //$stack-link-to:#contactchannelsendverificationemail - update(options): Promise; //$stack-link-to:#contactchannelupdateoptions - delete(): Promise; //$stack-link-to:#contactchanneldelete -};`} /> - - - - - The id of the contact channel as a `string`. - - - - ```typescript - declare const id: string; - ``` - - - - - - - - The value of the contact channel. If type is `"email"`, this is an email address. - - - - ```typescript - declare const value: string; - ``` - - - - - - - - The type of the contact channel. Currently always `"email"`. - - - - ```typescript - declare const type: 'email'; - ``` - - - - - - - - Indicates whether the contact channel is the user's primary contact channel. If an email is set to primary, it will be the value on the `user.primaryEmail` field. - - - - ```typescript - declare const isPrimary: boolean; - ``` - - - - - - - - Indicates whether the contact channel is verified. - - - - ```typescript - declare const isVerified: boolean; - ``` - - - - - - - - Indicates whether the contact channel is used for authentication. If set to `true`, the user can use this contact channel with OTP or password to sign in. - - - - ```typescript - declare const usedForAuth: boolean; - ``` - - - - - - - - Sends a verification email to this contact channel. Once the user clicks the verification link in the email, the contact channel will be marked as verified. - - ### Parameters - - None. - - ### Returns - - `Promise` - - - - - ```typescript - declare function sendVerificationEmail(): Promise; - ``` - - - ```typescript Sending verification email - await contactChannel.sendVerificationEmail(); - ``` - - - - - - - - - Updates the contact channel. After updating the value, the contact channel will be marked as unverified. - - ### Parameters - - - An object containing properties for updating. - - - The new value of the contact channel. - - - The new type of the contact channel. Currently always `"email"`. - - - Indicates whether the contact channel is used for authentication. - - - Indicates whether the contact channel is the user's primary contact channel. - - - - - ### Returns - - `Promise` - - - - - ```typescript - declare function update(options: { - value?: string; - type?: 'email'; - usedForAuth?: boolean; - isPrimary?: boolean; - }): Promise; - ``` - - - ```typescript Updating contact channel - await contactChannel.update({ - value: "new-email@example.com", - usedForAuth: true, - }); - ``` - - - - - - - - - Deletes the contact channel. - - ### Parameters - - None. - - ### Returns - - `Promise` - - - - - ```typescript - declare function delete(): Promise; - ``` - - - ```typescript Deleting contact channel - await contactChannel.delete(); - ``` - - - - +--- # `ServerContactChannel` @@ -236,73 +30,4 @@ Usually obtained by calling [`serverUser.listContactChannels()`](../types/user.m or [`serverUser.useContactChannels()`](../types/user.mdx#serveruserusecontactchannels) {/* THIS_LINE_PLATFORM react-like */} . -### Table of Contents - -; //$stack-link-to:#servercontactchannelupdateoptions - };`} /> - ---- - - - - - Updates the contact channel. - - This method is similar to the one on `ContactChannel`, but also allows setting the `isVerified` property. - - ### Parameters - - - An object containing properties for updating. - - - The new value of the contact channel. - - - The new type of the contact channel. Currently always `"email"`. - - - Indicates whether the contact channel is used for authentication. - - - Indicates whether the contact channel is verified. - - - Indicates whether the contact channel is the user's primary contact channel. - - - - - ### Returns - - `Promise` - - - - - ```typescript - declare function update(options: { - value?: string; - type?: 'email'; - usedForAuth?: boolean; - isVerified?: boolean; - isPrimary?: boolean; - }): Promise; - ``` - - - ```typescript Updating server contact channel - await serverContactChannel.update({ - value: "new-email@example.com", - usedForAuth: true, - isVerified: true, - }); - ``` - - - - + diff --git a/docs/content/docs/sdk/types/customer.mdx b/docs/content/docs/sdk/types/customer.mdx deleted file mode 100644 index 66bc01484c..0000000000 --- a/docs/content/docs/sdk/types/customer.mdx +++ /dev/null @@ -1,255 +0,0 @@ ---- -title: Customer -full: true ---- - -The `Customer` interface provides payment and item management functionality that is shared between users and teams. Both [`CurrentUser`](../types/user.mdx#currentuser) and [`Team`](../types/team.mdx#team) types extend this interface, allowing them to create checkout URLs and manage items. - -On this page: -- [Customer](#customer) - ---- - -# `Customer` - -The `Customer` interface defines the payment-related functionality available to both users and teams. It provides methods for creating checkout URLs for purchases and managing quantifiable items like credits, API calls, or subscription allowances. - -This interface is automatically available on: -- [`CurrentUser`](../types/user.mdx#currentuser) objects -- [`Team`](../types/team.mdx#team) objects -- [`ServerUser`](../types/user.mdx#serveruser) objects (with additional server-side capabilities) -- [`ServerTeam`](../types/team.mdx#serverteam) objects (with additional server-side capabilities) - -### Table of Contents - -; //$stack-link-to:#customercreatecheckouturl - getItem(itemId): Promise; //$stack-link-to:#customergetitem - // NEXT_LINE_PLATFORM react-like - ⤷ useItem(itemId): Item; //$stack-link-to:#customeruseitem -};`} /> - - - - - The unique identifier for the customer. For users, this is the user ID; for teams, this is the team ID. - - - - ```typescript - declare const id: string; - ``` - - - - - - - - Creates a secure checkout URL for purchasing a product. This method integrates with Stripe to generate a payment link that handles the entire purchase flow. - - The checkout URL will redirect users to a Stripe-hosted payment page where they can complete their purchase. After successful payment, users will be redirected back to your application. - - ### Parameters - - - Options for creating the checkout URL. - - - The ID of the product to purchase, as configured in your Stack Auth project settings. - - - - - ### Returns - - `Promise`: A secure URL that redirects to the Stripe checkout page for the specified product. - - - - - ```typescript - declare function createCheckoutUrl(options: { - productId: string; - }): Promise; - ``` - - - ```typescript User purchasing a subscription - const user = useUser({ or: "redirect" }); - - const handleUpgrade = async () => { - try { - const checkoutUrl = await user.createCheckoutUrl({ - productId: "prod_premium_monthly", - }); - - // Redirect to Stripe checkout - window.location.href = checkoutUrl; - } catch (error) { - console.error("Failed to create checkout URL:", error); - } - }; - ``` - - ```typescript Team purchasing additional seats - const team = await user.getTeam("team_123"); - - const purchaseSeats = async () => { - const checkoutUrl = await team.createCheckoutUrl({ - productId: "prod_additional_seats", - }); - - // Open checkout in new tab - window.open(checkoutUrl, '_blank'); - }; - ``` - - - - - - - - - Retrieves information about a specific item associated with this customer. Items represent quantifiable resources such as credits, API calls, storage quotas, or subscription allowances. - - ### Parameters - - - The ID of the item to retrieve, as configured in your Stack Auth project settings. - - - ### Returns - - `Promise`: An [`Item`](../types/item.mdx#item) object containing the display name, current quantity, and other details. - - - - - ```typescript - declare function getItem(itemId: string): Promise; - ``` - - - ```typescript Checking user credits - const user = useUser({ or: "redirect" }); - - const checkCredits = async () => { - const credits = await user.getItem("credits"); - console.log(`Available credits: ${credits.nonNegativeQuantity}`); - console.log(`Actual balance: ${credits.quantity}`); - }; - ``` - - ```typescript Checking team API quota - const team = await user.getTeam("team_123"); - const apiQuota = await team.getItem("api_calls"); - - if (apiQuota.nonNegativeQuantity < 100) { - console.warn("Team is running low on API calls"); - } - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Retrieves information about a specific item associated with this customer, used as a React hook. This provides real-time updates when the item quantity changes. - - ### Parameters - - - The ID of the item to retrieve. - - - ### Returns - - `Item`: An [`Item`](../types/item.mdx#item) object containing the display name, current quantity, and other details. - - - - - ```typescript - declare function useItem(itemId: string): Item; - ``` - - - ```typescript Real-time credits display - function CreditsWidget() { - const user = useUser({ or: "redirect" }); - const credits = user.useItem("credits"); - - return ( -
-

Available Credits

-
- {credits.nonNegativeQuantity} -
- {credits.displayName} -
- ); - } - ``` - - ```typescript Team quota monitoring - function TeamQuotaStatus({ teamId }: { teamId: string }) { - const user = useUser({ or: "redirect" }); - const team = user.useTeam(teamId); - const apiCalls = team.useItem("api_calls"); - - const usagePercentage = (apiCalls.quantity / 10000) * 100; - - return ( -
-
-
-
-

- {apiCalls.quantity.toLocaleString()} / 10,000 API calls used -

-
- ); - } - ``` - - - - -{/* END_PLATFORM */} - -## Usage Notes - -### Payment Flow - -When using `createCheckoutUrl()`, the typical flow is: - -1. **Create checkout URL**: Call `createCheckoutUrl()` with the desired product ID -2. **Redirect to Stripe**: Direct the user to the returned URL -3. **User completes payment**: Stripe handles the payment process -4. **Webhook processing**: Stack Auth receives webhook notifications from Stripe -5. **Item allocation**: Purchased items are automatically added to the customer's account -6. **User returns**: User is redirected back to your application - -### Item Management - -Items are automatically managed through the payment system: - -- **Purchases**: When a user completes a purchase, associated items are automatically added -- **Subscriptions**: Recurring subscriptions automatically replenish items at the specified intervals -- **Manual allocation**: Server-side code can manually adjust item quantities using [`ServerItem`](../types/item.mdx#serveritem) methods - -### Security Considerations - -- **Client-side safety**: All payment operations are designed to be safe for client-side use -- **Server validation**: Critical operations should always be validated on the server side -- **Race conditions**: Use [`tryDecreaseQuantity()`](../types/item.mdx#serveritemtrydecreasequantity) for atomic, race-condition-free item consumption diff --git a/docs/content/docs/sdk/types/email.mdx b/docs/content/docs/sdk/types/email.mdx index 488a154862..5fd6f98f3f 100644 --- a/docs/content/docs/sdk/types/email.mdx +++ b/docs/content/docs/sdk/types/email.mdx @@ -3,199 +3,64 @@ title: Email full: true --- -This is a detailed reference for email-related types in Stack Auth. If you're looking for a more high-level overview, please refer to our [guide on the email system](../../concepts/emails.mdx). +This is a detailed reference for email-related functionality in Stack Auth. If you're looking for a more high-level overview, please refer to our [guide on the email system](../../concepts/emails.mdx). -On this page: -- [SendEmailOptions](#sendemailoptions) +--- + +## Sending Emails + +Stack Auth provides server-side email sending capabilities through the `sendEmail` method on `StackServerApp`. + +### stackServerApp.sendEmail(options) + +Sends an email to one or more users. This method is only available on the server side with the `SECRET_SERVER_KEY`. + +See the full documentation on the [StackServerApp](../objects/stack-app.mdx#stackserverappsendemailoptions) page. + +**Parameters:** + +The `options` object accepts the following properties: + +- **userIds** (`string[]`, required) - An array of user IDs that will receive the email. All users must exist in your Stack Auth project. + +- **subject** (`string`, optional) - The email subject line. If using a template, this overrides the template's default subject. + +- **notificationCategoryName** (`string`, optional) - Notification category name for user preferences. Users can opt in or out of specific categories. + +**Email Content** (choose one): + +- **html** (`string`, optional) - Custom HTML content for the email. Cannot be used with `templateId` or `variables`. + +- **templateId** (`string`, optional) - ID of the email template to use. Cannot be used with `html`. + +- **variables** (`Record`, optional) - Variables to substitute in the template. Only used when `templateId` is provided. + +**Theme**: + +- **themeId** (`string | null | false`, optional) - Theme to apply. Use `null` for no theme, `false` for default theme, or a string ID for a specific theme. + +**Returns:** `Promise` + +**Examples:** + +#### Send HTML Email + + + +#### Send Template Email + + --- -# `SendEmailOptions` - -Options for sending emails via the `sendEmail` method on `StackServerApp`. - -### Table of Contents - -; //$stack-link-to:#sendemailoptionsvariables -};`} /> - - - - - An array of user IDs that will receive the email. All users must exist in your Stack Auth project. - - - - ```typescript - userIds: string[] - ``` - - - ```typescript - { - userIds: ['user-1', 'user-2', 'user-3'], - // ... other options - } - ``` - - - - - - - - - Optional theme ID to apply to the email. Use `null` for no theme, `false` to use the default theme, or a string ID for a specific theme. - - - - ```typescript - themeId?: string | null | false - ``` - - - ```typescript - { - themeId: 'corporate-theme-id', - // or - themeId: null, // no theme - // or - themeId: false, // default theme - // ... other options - } - ``` - - - - - - - - - Optional email subject line. If using a template, this overrides the template's default subject. - - - - ```typescript - subject?: string - ``` - - - ```typescript - { - subject: 'Welcome to our platform!', - // ... other options - } - ``` - - - - - - - - - Optional notification category name for user preferences. Users can opt in or out of specific categories through their account settings. - - - - ```typescript - notificationCategoryName?: string - ``` - - - ```typescript - { - notificationCategoryName: 'product_updates', - // ... other options - } - ``` - - - - - - - - - Custom HTML content for the email. Use this option when you want to send a custom HTML email instead of using a template. Cannot be used together with `templateId` or `variables`. - - - - ```typescript - html?: string - ``` - - - ```typescript - { - userIds: ['user-1'], - html: '

Welcome!

Thanks for joining us.

', - subject: 'Welcome to our platform' - } - ``` -
-
-
-
- - - - - ID of the email template to use. Use this option when you want to send a template-based email with variables. Cannot be used together with `html`. - - - - ```typescript - templateId?: string - ``` - - - ```typescript - { - userIds: ['user-1'], - templateId: 'welcome-template', - variables: { - userName: 'John Doe', - activationUrl: 'https://app.com/activate/token123' - } - } - ``` - - - - - - - - - Optional variables to substitute in the template. Only used when `templateId` is provided. - - - - ```typescript - variables?: Record - ``` - - - ```typescript - { - templateId: 'welcome-template', - variables: { - userName: 'John Doe', - activationUrl: 'https://app.com/activate/token123', - supportEmail: 'support@yourapp.com' - } - } - ``` - - - - + +**Note**: Email sending is only available server-side. The `sendEmail` method requires the `SECRET_SERVER_KEY` and is accessed through `StackServerApp`. `SendEmailOptions` is not a standalone type you can import - it's the parameter type for the `sendEmail()` method. + diff --git a/docs/content/docs/sdk/types/item.mdx b/docs/content/docs/sdk/types/item.mdx index acac24e4cc..02f8bcc0df 100644 --- a/docs/content/docs/sdk/types/item.mdx +++ b/docs/content/docs/sdk/types/item.mdx @@ -3,7 +3,7 @@ title: Item full: true --- -Items represent quantifiable resources in your application, such as credits, API calls, storage quotas, or subscription allowances. They can be associated with users, teams, or custom customers and are managed through Stack Auth's payment system. +Items represent quantifiable resources in your application, such as credits, API calls, storage quotas, or subscription allowances. On this page: - [Item](#item) @@ -13,224 +13,18 @@ On this page: # `Item` -The `Item` type represents a quantifiable resource that can be consumed or managed within your application. Items are typically obtained through purchases, subscriptions, or manual allocation. +Represents a quantifiable resource that users or teams can own and consume. Items can be retrieved through: -- [`user.getItem()`](../types/user.mdx#currentusergetitem) -- [`user.useItem()`](../types/user.mdx#currentuseruseitem) (React hook) -- [`team.getItem()`](../types/team.mdx#teamgetitem) -- [`team.useItem()`](../types/team.mdx#teamuseitem) (React hook) +- [`user.getItem(itemId)`](./user.mdx#currentusergetitem) / [`user.useItem(itemId)`](./user.mdx#currentuseruseitem) +- [`team.getItem(itemId)`](./team.mdx#teamgetitem) / [`team.useItem(itemId)`](./team.mdx#teamuseitem) -### Table of Contents - - - - - - - The human-readable name of the item as configured in your Stack Auth project settings. - - - - ```typescript - declare const displayName: string; - ``` - - - - - - - - The current quantity of the item. This value can be negative, which is useful for tracking overdrafts or pending charges. - - For example, if a user has 100 credits but makes a purchase that costs 150 credits, the quantity might temporarily be -50 until the purchase is processed. - - - - ```typescript - declare const quantity: number; - ``` - - - - - - - - The quantity clamped to a minimum of 0. This is equivalent to `Math.max(0, quantity)` and is useful for display purposes when you don't want to show negative values to users. - - Use this when you want to display available resources without confusing users with negative numbers. - - - - ```typescript - declare const nonNegativeQuantity: number; - ``` - - - + --- -
- # `ServerItem` -The `ServerItem` type extends `Item` with additional server-side methods for modifying quantities. This type is only available in server-side contexts and provides race-condition-safe operations for managing item quantities. - -Server items can be retrieved through: -- [`serverUser.getItem()`](../types/user.mdx#serverusergetitem) -- [`serverUser.useItem()`](../types/user.mdx#serveruseruseitem) (React hook) -- [`serverTeam.getItem()`](../types/team.mdx#serverteamgetitem) -- [`serverTeam.useItem()`](../types/team.mdx#serverteamuseitem) (React hook) - -### Table of Contents - -; //$stack-link-to:#serveritemincreasequantity - decreaseQuantity(amount): Promise; //$stack-link-to:#serveritemdecreasequantity - tryDecreaseQuantity(amount): Promise; //$stack-link-to:#serveritemtrydecreasequantity - };`} /> - - - - - Increases the item quantity by the specified amount. This operation is atomic and safe for concurrent use. - - ### Parameters - - - The amount to increase the quantity by. Must be a positive number. - - - ### Returns - - `Promise` - - - - - ```typescript - declare function increaseQuantity(amount: number): Promise; - ``` - - - ```typescript Adding credits to a user - const user = await stackServerApp.getUser({ userId: "user_123" }); - const credits = await user.getItem("credits"); - - // Add 100 credits - await credits.increaseQuantity(100); - ``` - - - - - - - - - Decreases the item quantity by the specified amount. This operation allows the quantity to go negative. - - **Note**: If you want to prevent the quantity from going below zero, use [`tryDecreaseQuantity()`](#serveritemtrydecreasequantity) instead, as it provides race-condition-free protection against negative quantities. - - ### Parameters - - - The amount to decrease the quantity by. Must be a positive number. - - - ### Returns - - `Promise` - - - - - ```typescript - declare function decreaseQuantity(amount: number): Promise; - ``` - - - ```typescript Consuming user credits - const user = await stackServerApp.getUser({ userId: "user_123" }); - const credits = await user.getItem("credits"); - - // Consume 50 credits (allows negative balance) - await credits.decreaseQuantity(50); - ``` - - - - - - - - - Attempts to decrease the item quantity by the specified amount, but only if the result would be non-negative. Returns `true` if the operation succeeded, `false` if it would result in a negative quantity. - - This method is race-condition-safe and is ideal for implementing prepaid credit systems where you need to ensure sufficient balance before allowing an operation. - - ### Parameters - - - The amount to decrease the quantity by. Must be a positive number. - - - ### Returns - - `Promise`: `true` if the quantity was successfully decreased, `false` if the operation would result in a negative quantity. - - - - - ```typescript - declare function tryDecreaseQuantity(amount: number): Promise; - ``` - - - ```typescript Safe credit consumption - const user = await stackServerApp.getUser({ userId: "user_123" }); - const credits = await user.getItem("credits"); - - // Try to consume 50 credits, only if sufficient balance - const success = await credits.tryDecreaseQuantity(50); - - if (success) { - console.log("Credits consumed successfully"); - // Proceed with the operation - } else { - console.log("Insufficient credits"); - // Handle insufficient balance - throw new Error("Not enough credits available"); - } - ``` +Like `Item`, but includes server-side quantity management methods that require the `SECRET_SERVER_KEY`. - ```typescript API rate limiting with credits - async function handleApiCall(userId: string) { - const user = await stackServerApp.getUser({ userId }); - const apiCalls = await user.getItem("api_calls"); - - // Check if user has API calls remaining - const canProceed = await apiCalls.tryDecreaseQuantity(1); - - if (!canProceed) { - throw new Error("API rate limit exceeded. Please upgrade your plan."); - } - - // Process the API call - return processApiRequest(); - } - ``` - - - - + diff --git a/docs/content/docs/sdk/types/team-permission.mdx b/docs/content/docs/sdk/types/team-permission.mdx index 89006acda6..e8f09d3208 100644 --- a/docs/content/docs/sdk/types/team-permission.mdx +++ b/docs/content/docs/sdk/types/team-permission.mdx @@ -3,28 +3,49 @@ title: TeamPermission full: true --- -The `TeamPermission` object represents a permission that a user has within a team. Currently, it contains only an `id` to specify the permission. +The `TeamPermission` object represents a permission that a user has within a team. It contains basic information about the permission assignment. -You can get `TeamPermission` objects by calling functions such as `user.getPermission(...)` or `user.listPermissions()`. +--- + +## Accessing Team Permissions + +You can get `TeamPermission` objects through the following methods on [`CurrentUser`](./user.mdx) or [`Team`](./team.mdx): + +### Permission Methods + +**Get a specific permission:** +- [`user.getPermission(scope, permissionId)`](./user.mdx#currentusergetpermission) +- [`user.usePermission(scope, permissionId)`](./user.mdx#currentuserusepermission) {/* THIS_LINE_PLATFORM react-like */} + +**List all permissions:** +- [`user.listPermissions(scope)`](./user.mdx#currentuserlistpermissions) +- [`user.usePermissions(scope)`](./user.mdx#currentuserusepermissions) {/* THIS_LINE_PLATFORM react-like */} + +**Check if user has permission:** +- [`user.hasPermission(scope, permissionId)`](./user.mdx#currentuserhaspermission) + +--- + +## Properties + +A `TeamPermission` object contains: + +- **id** (`string`) - The identifier of the permission + +--- + +## Server-Side Permissions + +On the server side with `SECRET_SERVER_KEY`, you can also: -### Table of Contents +**Grant permissions:** +- [`serverUser.grantPermission(scope, permissionId)`](./user.mdx#serverusergrantpermission) - +**Revoke permissions:** +- [`serverUser.revokePermission(scope, permissionId)`](./user.mdx#serveruserrevokepermission) --- - - - - The identifier of the permission as a `string`. - - - - ```typescript - declare const id: string; - ``` - - - + +**Note**: `TeamPermission` is not a standalone type you can import. It's returned by permission-related methods on `CurrentUser`, `ServerUser`, and `Team` objects. For server-side permission management with additional capabilities, see `AdminTeamPermission` (internal use only). + diff --git a/docs/content/docs/sdk/types/team-profile.mdx b/docs/content/docs/sdk/types/team-profile.mdx index bc9ce85ac3..90b66a3d8b 100644 --- a/docs/content/docs/sdk/types/team-profile.mdx +++ b/docs/content/docs/sdk/types/team-profile.mdx @@ -6,60 +6,25 @@ full: true This is a detailed reference for the `TeamProfile` and `ServerTeamProfile` objects. On this page: -- [TeamProfile](#teamprofile) -- [ServerTeamProfile](#serverteamprofile) - -# `TeamProfile` - -The `TeamProfile` object represents the profile of a user within the context of a team. It includes the user's profile information specific to the team and can be accessed through the `teamUser.teamProfile` property on a `TeamUser` object. - -### Table of Contents - - +- [TeamMemberProfile](#teammemberprofile) +- [ServerTeamMemberProfile](#serverteammemberprofile) --- - - - - The display name of the user within the team context as a `string` or `null` if no display name is set. - - +# `TeamMemberProfile` - ```typescript - declare const displayName: string | null; - ``` - - - +The `TeamMemberProfile` object represents the profile of a user within the context of a team. It includes the user's profile information specific to that team. - - - - The profile image URL of the user within the team context as a `string`, or `null` if no profile image is set. - - +You can access team profiles through: +- `teamUser.teamProfile` on a [`TeamUser`](./team-user.mdx) object +- `user.getTeamProfile(team)` / `user.useTeamProfile(team)` on a [`CurrentUser`](./user.mdx) object - ```typescript - declare const profileImageUrl: string | null; - ``` - - - + --- -# `ServerTeamProfile` +# `ServerTeamMemberProfile` -The `ServerTeamProfile` object is currently the same as `TeamProfile`. +Like `TeamMemberProfile`, but for server-side contexts with the `SECRET_SERVER_KEY`. -### Table of Contents - - - ---- + diff --git a/docs/content/docs/sdk/types/team-user.mdx b/docs/content/docs/sdk/types/team-user.mdx index 1271298087..3d16189df3 100644 --- a/docs/content/docs/sdk/types/team-user.mdx +++ b/docs/content/docs/sdk/types/team-user.mdx @@ -11,48 +11,11 @@ On this page: # `TeamUser` -The `TeamUser` object is used on the client side to represent a user in the context of a team, providing minimal information about the user, including their ID and team-specific profile. +The `TeamUser` object represents a user in the context of a team, providing minimal information about the user, including their ID and team-specific profile. -It is usually obtained by calling -`team.useUsers()` or {/* THIS_LINE_PLATFORM react-like */} -`team.listUsers()` on a [`Team` object](../types/team.mdx#team). +It is usually obtained by calling `team.useUsers()` {/* THIS_LINE_PLATFORM react-like */} or `team.listUsers()` on a [`Team` object](../types/team.mdx#team). -### Table of Contents - - - ---- - - - - - The ID of the user. - - - - ```typescript - declare const id: string; - ``` - - - - - - - - The team profile of the user as a `TeamProfile` object. - - - - ```typescript - declare const teamProfile: TeamProfile; - ``` - - - + --- @@ -62,29 +25,4 @@ The `ServerTeamUser` object is used on the server side to represent a user withi It is usually obtained by calling `serverTeam.listUsers()` on a [`ServerTeam` object](../types/team.mdx#serverteam). -### Table of Contents - - - ---- - - - - - The team profile of the user as a `ServerTeamProfile` object. - - - - ```typescript - declare const teamProfile: ServerTeamProfile; - ``` - - - + diff --git a/docs/content/docs/sdk/types/team.mdx b/docs/content/docs/sdk/types/team.mdx index 239b1e312b..d9d17a84a9 100644 --- a/docs/content/docs/sdk/types/team.mdx +++ b/docs/content/docs/sdk/types/team.mdx @@ -15,805 +15,14 @@ On this page: A `Team` object contains basic information and functions about a team, to the extent of which a member of the team would have access to it. -You can get `Team` objects with the -`user.useTeams()` or {/* THIS_LINE_PLATFORM react-like */} -`user.listTeams()` functions. The created team will then inherit the permissions of that user; for example, the `team.update(...)` function can only succeed if the user is allowed to make updates to the team. +You can get `Team` objects with the `user.useTeams()` {/* THIS_LINE_PLATFORM react-like */} or `user.listTeams()` functions. The created team will then inherit the permissions of that user; for example, the `team.update(...)` function can only succeed if the user is allowed to make updates to the team. -### Table of Contents - -; //$stack-link-to:#teamupdate - inviteUser(options): Promise; //$stack-link-to:#teaminviteuser - listUsers(): Promise; //$stack-link-to:#teamlistusers - // NEXT_LINE_PLATFORM react-like - ⤷ useUsers(): TeamUser[]; //$stack-link-to:#teamuseusers - listInvitations(): Promise<{ ... }[]>; //$stack-link-to:#teamlistinvitations - // NEXT_LINE_PLATFORM react-like - ⤷ useInvitations(): { ... }[]; //$stack-link-to:#teamuseinvitations - - createApiKey(options): Promise; //$stack-link-to:#teamcreateapikey - listApiKeys(): Promise; //$stack-link-to:#teamlistapikeys - // NEXT_LINE_PLATFORM react-like - ⤷ useApiKeys(): TeamApiKey[]; //$stack-link-to:#teamuseapikeys - - createCheckoutUrl(options): Promise; //$stack-link-to:#teamcreatecheckouturl - getItem(itemId): Promise; //$stack-link-to:#teamgetitem - // NEXT_LINE_PLATFORM react-like - ⤷ useItem(itemId): Item; //$stack-link-to:#teamuseitem -};`} /> - - - - - The team ID as a `string`. This value is always unique. - - - - ```typescript - declare const id: string; - ``` - - - - - - - - The display name of the team as a `string`. - - - - ```typescript - declare const displayName: string; - ``` - - - - - - - - The profile image URL of the team as a `string`, or `null` if no profile image is set. - - - - ```typescript - declare const profileImageUrl: string | null; - ``` - - - - - - - - The client metadata of the team as a `Json` object. - - - - ```typescript - declare const clientMetadata: Json; - ``` - - - - - - - - The client read-only metadata of the team as a `Json` object. - - - - ```typescript - declare const clientReadOnlyMetadata: Json; - ``` - - - - - - - - Updates the team information. - - Note that this operation requires the current user to have the `$update_team` permission. If the user lacks this permission, an error will be thrown. - - ### Parameters - - - The fields to update. - - - The display name of the team. - - - - The profile image URL of the team. - - - - The client metadata of the team. - - - - - ### Returns - - `Promise` - - - - - ```typescript - declare function update(options: { - displayName?: string; - profileImageUrl?: string | null; - clientMetadata?: Json; - }): Promise; - ``` - - - ```typescript Updating team details - await team.update({ - displayName: 'New Team Name', - profileImageUrl: 'https://example.com/profile.png', - clientMetadata: { - address: '123 Main St, Anytown, USA', - }, - }); - ``` - - - - - - - - - Sends an invitation email to a user to join the team. - - Note that this operation requires the current user to have the `$invite_members` permission. If the user lacks this permission, an error will be thrown. - - An invitation email containing a magic link will be sent to the specified user. If the user has an existing account, they will be automatically added to the team upon clicking the link. For users without an account, the link will guide them through the sign-up process before adding them to the team. - - ### Parameters - - - An object containing multiple properties. - - - The email of the user to invite. - - - - The URL where users will be redirected after accepting the team invitation. - - Required when calling `inviteUser()` in the server environment since the URL cannot be automatically determined. - - Example: `https://your-app-url.com/handler/team-invitation` - - - - - ### Returns - - `Promise` - - - - - ```typescript - declare function inviteUser(options: { - email: string; - callbackUrl?: string; - }): Promise; - ``` - - - ```typescript Sending a team invitation - await team.inviteUser({ - email: 'user@example.com', - }); - ``` - - - - - - - - - Gets a list of users in the team. - - Note that this operation requires the current user to have the `$read_members` permission. If the user lacks this permission, an error will be thrown. - - ### Parameters - - None. - - ### Returns - - `Promise` - - - - - ```typescript - declare function listUsers(): Promise; - ``` - - - ```typescript Listing team members - const users = await team.listUsers(); - users.forEach(user => { - console.log(user.id, user.teamProfile.displayName); - }); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Functionally equivalent to [`listUsers()`](#teamlistusers), but as a React hook. - - ### Parameters - - None. - - ### Returns - - `TeamUser[]` - - - - - ```typescript - declare function useUsers(): TeamUser[]; - ``` - - - ```typescript Listing team members in React component - const users = team.useUsers(); - users.forEach(user => { - console.log(user.id, user.teamProfile.displayName); - }); - ``` - - - - -{/* END_PLATFORM */} - - - - - Gets a list of invitations to the team. - - Note that this operation requires the current user to have the `$read_members` and `$invite_members` permissions. If the user lacks this permission, an error will be thrown. - - ### Parameters - - None. - - ### Returns - - `Promise<{ id: string, email: string, expiresAt: Date }[]>` - - - - - ```typescript - declare function listInvitations(): Promise<{ id: string, email: string, expiresAt: Date }[]>; - ``` - - - ```typescript Listing team invitations - const invitations = await team.listInvitations(); - invitations.forEach(invitation => { - console.log(invitation.id, invitation.email); - }); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Functionally equivalent to [`listInvitations()`](#teamlistinvitations), but as a React hook. - - ### Parameters - - None. - - ### Returns - - `{ id: string, email: string, expiresAt: Date }[]` - - - - - ```typescript - declare function useInvitations(): { id: string, email: string, expiresAt: Date }[]; - ``` - - - ```typescript Listing team invitations in React component - const invitations = team.useInvitations(); - invitations.forEach(invitation => { - console.log(invitation.id, invitation.email); - }); - ``` - - - - -{/* END_PLATFORM */} - - - - - Creates a new API key for the team. - - ### Parameters - - - An object containing multiple properties. - - - The name of the API key. - - - - The description of the API key. - - - - The expiration date of the API key. - - - - - ### Returns - - `Promise` - - - - - ```typescript - declare function createApiKey(options: { - name: string; - description: string; - expiresAt: Date; - }): Promise; - ``` - - - ```typescript Creating a new API key - await team.createApiKey({ - name: 'New API Key', - description: 'This is a new API key', - expiresAt: new Date('2024-01-01'), - }); - ``` - - - - - - - - - Gets a list of API keys for the team. - - ### Parameters - - None. - - ### Returns - - `Promise` - - - - - ```typescript - declare function listApiKeys(): Promise; - ``` - - - ```typescript Listing API keys - const apiKeys = await team.listApiKeys(); - apiKeys.forEach(key => { - console.log(key.id, key.name); - }); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Functionally equivalent to [`listApiKeys()`](#teamlistapikeys), but as a React hook. - - ### Parameters - - None. - - ### Returns - - `TeamApiKey[]` - - - - - ```typescript - declare function useApiKeys(): TeamApiKey[]; - ``` - - - ```typescript Using API keys in React component - const apiKeys = team.useApiKeys(); - apiKeys.forEach(key => { - console.log(key.id, key.name); - }); - ``` - - - - -{/* END_PLATFORM */} - - - - - Creates a checkout URL for the team to purchase products. This method integrates with Stripe to generate a secure payment link for team-level purchases. - - Note that this operation requires the current user to have appropriate permissions for team purchases. The specific permission requirements depend on your project configuration. - - ### Parameters - - - Options for creating the checkout URL. - - - The ID of the product to purchase. - - - - - ### Returns - - `Promise`: A URL that redirects to the Stripe checkout page for the specified product. - - - - - ```typescript - declare function createCheckoutUrl(options: { - productId: string; - }): Promise; - ``` - - - ```typescript Team purchasing additional seats - const checkoutUrl = await team.createCheckoutUrl({ - productId: "prod_team_seats", - }); - - // Redirect to checkout - window.location.href = checkoutUrl; - ``` - - - - - - - - - Retrieves information about a specific item (such as credits, API quotas, storage limits, etc.) for the team. - - ### Parameters - - - The ID of the item to retrieve. - - - ### Returns - - `Promise`: The item object containing display name, quantity, and other details. - - - - - ```typescript - declare function getItem(itemId: string): Promise; - ``` - - - ```typescript Checking team API quota - const apiQuota = await team.getItem("api_calls"); - console.log(`Team has ${apiQuota.quantity} API calls remaining`); - - if (apiQuota.nonNegativeQuantity < 100) { - console.warn("Team is running low on API calls"); - } - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Retrieves information about a specific item for the team, used as a React hook. - - ### Parameters - - - The ID of the item to retrieve. - - - ### Returns - - `Item`: The item object containing display name, quantity, and other details. - - - - - ```typescript - declare function useItem(itemId: string): Item; - ``` - - - ```typescript Team quota monitoring component - function TeamQuotaDisplay({ team }: { team: Team }) { - const storage = team.useItem("storage_gb"); - - return ( -
-

Team Storage Usage

-

{storage.quantity} GB used

-

Plan: {storage.displayName}

-
- ); - } - ``` -
-
-
-
-{/* END_PLATFORM */} + --- # `ServerTeam` -Like [`Team`](#team), but with [server permissions](../../concepts/stack-app.mdx#client-vs-server). Has full read and write access to everything. - -Calling `serverUser.getTeam(...)` and `serverUser.listTeams()` will return `ServerTeam` objects if the user is a [`ServerUser`](../types/user.mdx#serveruser). Alternatively, you can call `stackServerApp.getTeam('team_id_123')` or `stackServerApp.listTeams()` to query all teams of the project. - -`ServerTeam` extends the `Team` object, providing additional functions and properties as detailed below. It's important to note that while the `Team` object's functions may require specific user permissions, the corresponding functions in `ServerTeam` can be executed without these permission checks. This allows for more flexible and unrestricted team management on the server side. - -### Table of Contents - -; //$stack-link-to:#serverteamlistusers - // NEXT_LINE_PLATFORM react-like - ⤷ useUsers(): ServerTeamUser[]; //$stack-link-to:#serverteamuseusers - addUser(userId): Promise; //$stack-link-to:#serverteamadduseruserid - removeUser(userId): Promise; //$stack-link-to:#serverteamremoveuseruserid - delete(): Promise; //$stack-link-to:#serverteamdelete - };`} /> - - - - - The date and time when the team was created. - - - ```typescript - declare const createdAt: Date; - ``` - - - - - - - - The server metadata of the team as a `Json` object. - - - ```typescript - declare const serverMetadata: Json; - ``` - - - - - - - - Gets a list of users in the team. - - This is similar to the `listUsers` method on the `Team` object, but it returns `ServerTeamUser` objects instead of `TeamUser` objects and does not require any permissions. - - ### Parameters - - None. - - ### Returns - - `Promise` - - - - - ```typescript - declare function listUsers(): Promise; - ``` - - - ```typescript Listing server team members - const users = await team.listUsers(); - users.forEach(user => { - console.log(user.id, user.teamProfile.displayName); - }); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Functionally equivalent to [`listUsers()`](#serverteamlistusers), but as a React hook. - - ### Parameters - - None. - - ### Returns - - `ServerTeamUser[]` - - - - - ```typescript - declare function useUsers(): ServerTeamUser[]; - ``` - - - ```typescript Using server team members in React component - const users = team.useUsers(); - users.forEach(user => { - console.log(user.id, user.teamProfile.displayName); - }); - ``` - - - - -{/* END_PLATFORM */} - - - - - Adds a user to the team directly without sending an invitation email. - - ### Parameters - - - The ID of the user to add. - - - ### Returns - - `Promise` - - - - - ```typescript - declare function addUser(userId: string): Promise; - ``` - - - ```typescript Adding a user to the team - await team.addUser('user_id_123'); - ``` - - - - - - - - - Removes a user from the team. - - ### Parameters - - - The ID of the user to remove. - - - ### Returns - - `Promise` - - - - - ```typescript - declare function removeUser(userId: string): Promise; - ``` - - ### Examples - - ```typescript Removing a user from the team - await team.removeUser('user_id_123'); - ``` - - - - - - - - - Deletes the team. - - ### Parameters - - None. - - ### Returns - - `Promise` - - - +Like `Team`, but includes additional methods and properties that require the `SECRET_SERVER_KEY`. - ```typescript - declare function delete(): Promise; - ``` - - - ```typescript Deleting a team - await team.delete(); - ``` - - - - + diff --git a/docs/content/docs/sdk/types/user.mdx b/docs/content/docs/sdk/types/user.mdx index 7c9c6d4da8..59a998e254 100644 --- a/docs/content/docs/sdk/types/user.mdx +++ b/docs/content/docs/sdk/types/user.mdx @@ -14,1994 +14,20 @@ On this page: Use `useUser()` to get `CurrentUser` (client). Use `stackServerApp.getUser()` to get `CurrentServerUser` (server). -### Table of Contents - -; //$stack-link-to:#currentuserupdate - updatePassword(data): Promise; //$stack-link-to:#currentuserupdatepassword - getAuthHeaders(): Promise>; //$stack-link-to:#currentusergetauthheaders - getAuthJson(): Promise<{ accessToken: string | null }>; //$stack-link-to:#currentusergetauthjson - signOut([options]): Promise; //$stack-link-to:#currentusersignout - delete(): Promise; //$stack-link-to:#currentuserdelete - - getTeam(id): Promise; //$stack-link-to:#currentusergetteam - // NEXT_LINE_PLATFORM react-like - ⤷ useTeam(id): Team | null; //$stack-link-to:#currentuseruseteam - listTeams(): Promise; //$stack-link-to:#currentuserlistteams - // NEXT_LINE_PLATFORM react-like - ⤷ useTeams(): Team[]; //$stack-link-to:#currentuseruseteams - setSelectedTeam(team): Promise; //$stack-link-to:#currentusersetselectedteam - createTeam(data): Promise; //$stack-link-to:#currentusercreateteam - leaveTeam(team): Promise; //$stack-link-to:#currentuserleaveteam - getTeamProfile(team): Promise; //$stack-link-to:#currentusergetteamprofile - // NEXT_LINE_PLATFORM react-like - ⤷ useTeamProfile(team): EditableTeamMemberProfile; //$stack-link-to:#currentuseruseteamprofile - - hasPermission(scope, permissionId): Promise; //$stack-link-to:#currentuserhaspermission - getPermission(scope, permissionId[, options]): Promise; //$stack-link-to:#currentusergetpermission - // NEXT_LINE_PLATFORM react-like - ⤷ usePermission(scope, permissionId[, options]): TeamPermission | null; //$stack-link-to:#currentuserusepermission - listPermissions(scope[, options]): Promise; //$stack-link-to:#currentuserlistpermissions - // NEXT_LINE_PLATFORM react-like - ⤷ usePermissions(scope[, options]): TeamPermission[]; //$stack-link-to:#currentuserusepermissions - - listContactChannels(): Promise; //$stack-link-to:#currentuserlistcontactchannels - // NEXT_LINE_PLATFORM react-like - ⤷ useContactChannels(): ContactChannel[]; //$stack-link-to:#currentuserusecontactchannels - - getConnectedAccount(account): Promise; //$stack-link-to:#currentusergetconnectedaccount - // NEXT_LINE_PLATFORM react-like - ⤷ useConnectedAccount(account): OAuthConnection | null; //$stack-link-to:#currentuseruseconnectedaccount - listConnectedAccounts(): Promise; //$stack-link-to:#currentuserlistconnectedaccounts - // NEXT_LINE_PLATFORM react-like - ⤷ useConnectedAccounts(): OAuthConnection[]; //$stack-link-to:#currentuseruseconnectedaccounts - linkConnectedAccount(provider[, options]): Promise; //$stack-link-to:#currentuserlinkconnectedaccount - getOrLinkConnectedAccount(provider[, options]): Promise; //$stack-link-to:#currentusergetorlinkconnectedaccount - // NEXT_LINE_PLATFORM react-like - ⤷ useOrLinkConnectedAccount(provider[, options]): OAuthConnection; //$stack-link-to:#currentuseruseorlinkconnectedaccount - - createApiKey(options): Promise; //$stack-link-to:#currentusercreateapikey - listApiKeys(): Promise; //$stack-link-to:#currentuserlistapikeys - // NEXT_LINE_PLATFORM react-like - ⤷ useApiKeys(): UserApiKey[]; //$stack-link-to:#currentuseruseapikeys - - createCheckoutUrl(options): Promise; //$stack-link-to:#currentusercreatecheckouturl - getItem(itemId): Promise; //$stack-link-to:#currentusergetitem - // NEXT_LINE_PLATFORM react-like - ⤷ useItem(itemId): Item; //$stack-link-to:#currentuseruseitem -};`} /> - - - - - The user ID as a `string`. This is the unique identifier of the user. - - - - ```typescript - declare const id: string; - ``` - - - - - - - - The display name of the user as a `string` or `null` if not set. The user can modify this value. - - - - ```typescript - declare const displayName: string | null; - ``` - - - - - - - - The primary email of the user as a `string` or `null`. Note that this is not necessarily unique. - - - - ```typescript - declare const primaryEmail: string | null; - ``` - - - - - - - - A `boolean` indicating whether the primary email of the user is verified. - - - - ```typescript - declare const primaryEmailVerified: boolean; - ``` - - - - - - - - The profile image URL of the user as a `string` or `null` if no profile image is set. - - - - ```typescript - declare const profileImageUrl: string | null; - ``` - - - - - - - - The date and time when the user signed up, as a `Date`. - - - - ```typescript - declare const signedUpAt: Date; - ``` - - - - - - - - A `boolean` indicating whether the user has a password set. - - - - ```typescript - declare const hasPassword: boolean; - ``` - - - - - - - - The client metadata of the user as an `object`. This metadata is visible on the client side but should not contain sensitive or server-only information. - - - - ```typescript - declare const clientMetadata: Json; - ``` - - - - - - - - Read-only metadata visible on the client side. This metadata can only be modified on the server side. - - - - ```typescript - declare const clientReadOnlyMetadata: Json; - ``` - - - - - - - - The currently selected team for the user, if applicable, as a `Team` object or `null` if no team is selected. - - - - ```typescript - declare const selectedTeam: Team | null; - ``` - - - - - - - - Updates the user information. - - ### Parameters - - - The fields to update. - - - The new display name for the user. - - - Custom metadata visible to the client. - - - The ID of the team to set as selected, or `null` to clear selection. - - - The URL of the user's new profile image, or `null` to remove it. - - - - - ### Returns - - `Promise` - - - - ```typescript - declare function update(data: { - displayName?: string; - clientMetadata?: Json; - selectedTeamId?: string | null; - profileImageUrl?: string | null; - }): Promise; - ``` - - - ```typescript Updating user details - await user.update({ - displayName: "New Display Name", - clientMetadata: { - address: "123 Main St", - }, - }); - ``` - - - - - - - - - Gets the team with the specified ID. - - ### Parameters - - - The ID of the team to get. - - - ### Returns - - `Promise`: The team object, or `null` if the team is not found or the user is not a member of the team. - - - - - ```typescript - declare function getTeam(id: string): Promise; - ``` - - - ```typescript Getting a team by ID - const team = await user.getTeam("teamId"); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Gets the team with the given ID. This is the same as `getTeam` but is used as a React hook. - - ### Parameters - - - The ID of the team to get. - - - ### Returns - - `Team | null`: The team object, or `null` if the team is not found or the user is not a member of the team. - - - - ```typescript - declare function useTeam(id: string): Team | null; - ``` - - - - ```typescript Using a team in a React component - const team = user.useTeam("teamId"); - ``` - - - - -{/* END_PLATFORM */} - - - - - Lists all the teams the user is a member of. - - ### Parameters - - None. - - ### Returns - - `Promise`: The list of teams. - - - - - ```typescript - declare function listTeams(): Promise; - ``` - - - ```typescript Listing all teams - const teams = await user.listTeams(); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Lists all the teams the user is a member of. This is the same as `listTeams` but is used as a React hook. - - ### Parameters - - None. - - ### Returns - - `Team[]`: The list of teams. - - - - - ```typescript - declare function useTeams(): Team[]; - ``` - - - ```typescript Using teams in a React component - const teams = user.useTeams(); - ``` - - - - -{/* END_PLATFORM */} - - - - - Sets the currently selected team for the user. - - ### Parameters - - - The team to set as selected, or `null` to clear selection. - - - ### Returns - - `Promise` - - - - - ```typescript - declare function setSelectedTeam(team: Team | null): Promise; - ``` - - - ```typescript Setting the selected team - const team = await user.getTeam("team_id_123"); - await user.setSelectedTeam(team); - ``` - - - - - - - - - Creates a new team for the user. The user will be added to the team and given creator permissions. - - **Note**: If client-side team creation is disabled in the Stack dashboard, this will throw an error. - - ### Parameters - - - The data for creating the team. - - - The display name for the team. - - - The URL of the team's profile image, or `null` to remove it. - - - - - ### Returns - - `Promise`: The created team. - - - - - ```typescript - declare function createTeam(data: { - displayName: string; - profileImageUrl?: string | null; - }): Promise; - ``` - - - ```typescript Creating a new team - const team = await user.createTeam({ - displayName: "New Team", - profileImageUrl: "https://example.com/profile.jpg", - }); - ``` - - - - - - - - - Allows the user to leave a team. If the user is not a member of the team, this will throw an error. - - ### Parameters - - - The team to leave. - - - ### Returns - - `Promise` - - - - - ```typescript - declare function leaveTeam(team: Team): Promise; - ``` - - - ```typescript Leaving a team - await user.leaveTeam(team); - ``` - - - - - - - - - Retrieves the user's profile within a specific team. - - ### Parameters - - - The team to retrieve the profile for. - - - ### Returns - - `Promise`: The user's editable profile for the specified team. - - - - - ```typescript - declare function getTeamProfile(team: Team): Promise; - ``` - - - ```typescript Getting a team profile - const profile = await user.getTeamProfile(team); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Retrieves the user's profile within a specific team. This is the same as `getTeamProfile` but is used as a React hook. - - ### Parameters - - - The team to retrieve the profile for. - - - ### Returns - - `EditableTeamMemberProfile`: The user's editable profile for the specified team. - - - - - ```typescript - declare function useTeamProfile(team: Team): EditableTeamMemberProfile; - ``` - - - ```typescript Using a team profile in React - const profile = user.useTeamProfile(team); - ``` - - - - -{/* END_PLATFORM */} - - - - - Checks if the user has a specific permission for a team. - - ### Parameters - - - The team to check the permission for. - - - The ID of the permission to check. - - - ### Returns - - `Promise`: Whether the user has the specified permission. - - - - - ```typescript - declare function hasPermission(scope: Team, permissionId: string): Promise; - ``` - - - ```typescript Checking user permission - const hasPermission = await user.hasPermission(team, "permissionId"); - ``` - - - - - - - - - Retrieves a specific permission for a user within a team. - - ### Parameters - - - The team to retrieve the permission for. - - - The ID of the permission to retrieve. - - - An object containing multiple properties. - - - Whether to retrieve the permission recursively. Default is `true`. - - - - - ### Returns - - `Promise`: The permission object, or `null` if not found. - - - - - ```typescript - declare function getPermission(scope: Team, permissionId: string, options?: { recursive?: boolean }): Promise; - ``` - - - ```typescript Getting a permission - const permission = await user.getPermission(team, "read_secret_info"); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Retrieves a specific permission for a user within a team, used as a React hook. - - ### Parameters - - - The team to retrieve the permission for. - - - The ID of the permission to retrieve. - - - An object containing multiple properties. - - - Whether to retrieve the permission recursively. Default is `true`. - - - - - ### Returns - - `TeamPermission | null`: The permission object, or `null` if not found. - - - - - ```typescript - declare function usePermission(scope: Team, permissionId: string, options?: { recursive?: boolean }): TeamPermission | null; - ``` - - - ```typescript Using a permission in React - const permission = user.usePermission(team, "read_secret_info"); - ``` - - - - -{/* END_PLATFORM */} - - - - - Lists all permissions the user has for a specified team. - - ### Parameters - - - The team to list permissions for. - - - An object containing multiple properties. - - - Whether to list the permissions recursively. Default is `true`. - - - - - ### Returns - - `Promise`: An array of permissions. - - - - - ```typescript - declare function listPermissions(scope: Team, options?: { recursive?: boolean }): Promise; - ``` - - - ```typescript Listing user permissions - const permissions = await user.listPermissions(team); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Lists all permissions the user has for a specified team, used as a React hook. - - ### Parameters - - - The team to retrieve permissions for. - - - An object containing multiple properties. - - - Whether to list the permissions recursively. Default is `true`. - - - - - ### Returns - - `TeamPermission[]`: An array of permissions. - - - - - ```typescript - declare function usePermissions(scope: Team, options?: { recursive?: boolean }): TeamPermission[]; - ``` - - - ```typescript Using permissions in a React component - const permissions = user.usePermissions(team); - ``` - - - - -{/* END_PLATFORM */} - - - - - Lists all the contact channels of the user. - - ### Parameters - - No parameters. - - ### Returns - - `Promise`: An array of contact channels. - - - - - ```typescript - declare function listContactChannels(): Promise; - ``` - - - ```typescript Listing contact channels - const contactChannels = await user.listContactChannels(); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Lists all the contact channels of the user, used as a React hook. - - ### Parameters - - No parameters. - - ### Returns - - `ContactChannel[]`: An array of contact channels. - - - - - ```typescript - declare function useContactChannels(): ContactChannel[]; - ``` - - - ```typescript Using contact channels in React - const contactChannels = user.useContactChannels(); - ``` - - - - -{/* END_PLATFORM */} - - - - - Gets a specific connected account by provider and provider account ID. Returns `null` if not found. - - For more details on connected accounts, see the [ConnectedAccount type reference](../types/connected-account.mdx) and the [OAuth guide](../../apps/oauth). - - ### Parameters - - - - - The provider config ID (e.g., `"google"`, `"github"`). - - - The account ID from the OAuth provider. - - - - - ### Returns - - `Promise` - - - - - ```typescript - declare function getConnectedAccount(account: { - provider: string, - providerAccountId: string, - }): Promise; - ``` - - - ```typescript Getting a connected account - const account = await user.getConnectedAccount({ - provider: "google", - providerAccountId: "123456", - }); - if (account) { - const result = await account.getAccessToken(); - } - ``` - - - - - -{/* IF_PLATFORM react-like */} - - - - React hook version of `getConnectedAccount`. Returns a specific connected account by provider and provider account ID, or `null` if not found. - - ### Parameters - - - - - The provider config ID (e.g., `"google"`, `"github"`). - - - The account ID from the OAuth provider. - - - - - ### Returns - - `OAuthConnection | null` - - - - - ```typescript - declare function useConnectedAccount(account: { - provider: string, - providerAccountId: string, - }): OAuthConnection | null; - ``` - - - ```tsx Using a connected account in React - const account = user.useConnectedAccount({ - provider: "google", - providerAccountId: "123456", - }); - ``` - - - - -{/* END_PLATFORM */} - - - - - Lists all connected accounts for this user. Only returns accounts for providers that have `allowConnectedAccounts` enabled. - - ### Parameters - - None. - - ### Returns - - `Promise` - - - - - ```typescript - declare function listConnectedAccounts(): Promise; - ``` - - - ```typescript Listing connected accounts - const accounts = await user.listConnectedAccounts(); - for (const account of accounts) { - console.log(account.provider, account.providerAccountId); - } - ``` - - - - - -{/* IF_PLATFORM react-like */} - - - - React hook to list all connected accounts for this user. - - ### Parameters - - None. - - ### Returns - - `OAuthConnection[]` - - - - - ```typescript - declare function useConnectedAccounts(): OAuthConnection[]; - ``` - - - ```tsx Using connected accounts in React - const accounts = user.useConnectedAccounts(); - ``` - - - - -{/* END_PLATFORM */} - - - - - Redirects the user to the OAuth flow to link a new connected account. This function always redirects and never returns. - - ### Parameters - - - The provider to link (e.g., `"google"`, `"github"`). - - - - - OAuth scopes to request during the linking flow. - - - - - ### Returns - - `Promise` (always redirects) - - - - - ```typescript - declare function linkConnectedAccount( - provider: string, - options?: { scopes?: string[] }, - ): Promise; - ``` - - - ```typescript Linking a Google account - await user.linkConnectedAccount("google", { - scopes: ["https://www.googleapis.com/auth/drive.readonly"], - }); - ``` - - - - - - - - - Gets a connected account for the given provider, or redirects the user to the OAuth flow to link one if none exists or the token/scopes are insufficient. - - ### Parameters - - - The provider to get or link (e.g., `"google"`, `"github"`). - - - - - OAuth scopes to require. If the existing account doesn't have these scopes, the user will be redirected to re-authorize. - - - - - ### Returns - - `Promise` - - - - - ```typescript - declare function getOrLinkConnectedAccount( - provider: string, - options?: { scopes?: string[] }, - ): Promise; - ``` - - - ```typescript Get or link a Google account with Drive access - const account = await user.getOrLinkConnectedAccount("google", { - scopes: ["https://www.googleapis.com/auth/drive.readonly"], - }); - const result = await account.getAccessToken(); - ``` - - - - - -{/* IF_PLATFORM react-like */} - - - - React hook version of `getOrLinkConnectedAccount`. Returns a connected account for the given provider, or redirects the user to link one if none exists or the token/scopes are insufficient. - - ### Parameters - - - The provider to get or link (e.g., `"google"`, `"github"`). - - - - - OAuth scopes to require. - - - - - ### Returns - - `OAuthConnection` - - - - - ```typescript - declare function useOrLinkConnectedAccount( - provider: string, - options?: { scopes?: string[] }, - ): OAuthConnection; - ``` - - - ```tsx Using or linking a connected account in React - const account = user.useOrLinkConnectedAccount("google", { - scopes: ["https://www.googleapis.com/auth/drive.readonly"], - }); - const result = account.useAccessToken(); - ``` - - - - -{/* END_PLATFORM */} - - - - - Creates a new API key for the user, which can be used for programmatic access to your application's backend. - - ### Parameters - - - Options for creating the API key. - - - A human-readable description of the API key's purpose. - - - The date when the API key will expire. Use null for keys that don't expire. - - - Whether the API key is public. Defaults to false. - - - **Secret API Keys** (default) begin with `sk_` and are monitored by Stack Auth's secret scanner, which can revoke them if detected in public code repositories. - - **Public API Keys** begin with `pk_` and are designed for client-side code where exposure is not a concern. - - - - - ### Returns - - `Promise`: The newly created API key. Note that this is the only time the full API key value will be visible. - - - - - ```typescript - declare function createApiKey(options: { - description: string; - expiresAt: Date | null; - isPublic?: boolean; - }): Promise; - ``` - - - ```typescript Creating an API key - const apiKey = await user.createApiKey({ - description: "Backend integration", - expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days - isPublic: false, - }); - - // Save the API key value securely, as it won't be retrievable later - console.log("API Key:", apiKey.value); - ``` - - - - - - - - - Lists all API keys that belong to the user. - - ### Parameters - - None. - - ### Returns - - `Promise`: An array of API keys belonging to the user. - - - - - ```typescript - declare function listApiKeys(): Promise; - ``` - - - ```typescript Listing API keys - const apiKeys = await user.listApiKeys(); - console.log(`You have ${apiKeys.length} API keys`); - - // Find keys that are about to expire - const soonToExpire = apiKeys.filter(key => - key.expiresAt && key.expiresAt.getTime() - Date.now() < 7 * 24 * 60 * 60 * 1000 - ); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Lists all API keys that belong to the user, used as a React hook. - - ### Parameters - - None. - - ### Returns - - `UserApiKey[]`: An array of API keys belonging to the user. - - - - - ```typescript - declare function useApiKeys(): UserApiKey[]; - ``` - - - ```typescript Using API keys in a React component - function ApiKeysList() { - const user = useUser(); - const apiKeys = user.useApiKeys(); - - return ( -
-

Your API Keys ({apiKeys.length})

-
    - {apiKeys.map(key => ( -
  • - {key.description} - Last four: {key.value.lastFour} - {key.isValid() ? ' (valid)' : ` (invalid: ${key.whyInvalid()})`} -
  • - ))} -
-
- ); - } - ``` -
-
-
-
-{/* END_PLATFORM */} - - - - - Updates the user's password. - - ### Parameters - - - The fields required for updating the password. - - - The current password of the user. - - - The new password for the user. - - - - - ### Returns - - `Promise`: Returns an error object if the operation fails, otherwise returns `void`. - - - - - ```typescript - declare function updatePassword(data: { - oldPassword: string; - newPassword: string; - }): Promise; - ``` - - - ```typescript Updating user password - const error = await user.updatePassword({ - oldPassword: "currentPassword", - newPassword: "newPassword", - }); - if (error) { - console.error("Error updating password", error); - } else { - console.log("Password updated successfully"); - } - ``` - - - - - - - - - Returns headers for sending authenticated HTTP requests to external servers. Most commonly used in cross-origin - requests. Similar to `getAuthJson`, but specifically for HTTP requests. - - If you are using `tokenStore: "cookie"`, you don't need this for same-origin requests. However, most - browsers now disable third-party cookies by default, so we must pass authentication tokens by header instead - if the client and server are on different origins. - - This function returns a header object that can be used with `fetch` or other HTTP request libraries to send - authenticated requests. - - On the server, you can then pass in the `Request` object to the `tokenStore` option - of your Stack app. Please note that CORS does not allow most headers by default, so you - must include `x-stack-auth` in the [`Access-Control-Allow-Headers` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) - of the CORS preflight response. - - ### Parameters - - No parameters. - - ### Returns - - `Promise>`: An object containing the authentication headers. - - - - - ```typescript - declare function getAuthHeaders(): Promise>; - ``` - - - ```typescript Passing auth headers to an external server - // client - const res = await fetch("https://api.example.com", { - headers: { - ...await user.getAuthHeaders() - // you can also add your own headers here - }, - }); - - // server - function handleRequest(req: Request) { - const user = await stackServerApp.getUser({ tokenStore: req }); - return new Response("Welcome, " + user.displayName); - } - ``` - - - - - - - - - Creates a JSON-serializable object containing the information to authenticate a user on an external server. - - While `getAuthHeaders` is the recommended way to send authentication tokens over HTTP, your app may use - a different protocol, for example WebSockets or gRPC. This function returns a token object that can be JSON-serialized and sent to the server in any way you like. - - On the server, you can pass in this token object into the `tokenStore` option to fetch user details. - - ### Parameters - - No parameters. - - ### Returns - - `Promise<{ accessToken: string | null }>`: An object containing the access token. - - - - - ```typescript - declare function getAuthJson(): Promise<{ accessToken: string | null }>; - ``` - - - ```typescript Passing auth tokens over an RPC call - // client - const res = await rpcCall(rpcEndpoint, { - data: { - auth: await user.getAuthJson(), - }, - }); - - // server - function handleRequest(data) { - const user = await stackServerApp.getUser({ tokenStore: data.auth }); - return new Response("Welcome, " + user.displayName); - } - ``` - - - - - - - - - Signs out the user and clears the session. - - ### Parameters - - - An object containing multiple properties. - - - The URL to redirect to after signing out. Defaults to the `afterSignOut` URL from the Stack app's `urls` object. - - - - - ### Returns - - `Promise` - - - - - ```typescript - declare function signOut(options?: { redirectUrl?: string }): Promise; - ``` - - - ```typescript Signing out - await user.signOut(); - ``` - - - - - - - - - Deletes the user. This action is irreversible and can only be used if client-side user deletion is enabled in the Stack dashboard. - - ### Parameters - - No parameters. - - ### Returns - - `Promise` - - - - - ```typescript - declare function delete(): Promise; - ``` - - - ```typescript Deleting the user - await user.delete(); - ``` - - - - - - - - - Creates a checkout URL for purchasing a product. This method integrates with Stripe to generate a secure payment link. - - ### Parameters - - - Options for creating the checkout URL. - - - The ID of the product to purchase. - - - - - ### Returns - - `Promise`: A URL that redirects to the Stripe checkout page for the specified product. - - - - - ```typescript - declare function createCheckoutUrl(options: { - productId: string; - }): Promise; - ``` - - - ```typescript Creating a checkout URL - const checkoutUrl = await user.createCheckoutUrl({ - productId: "prod_premium_plan", - }); - - // Redirect user to checkout - window.location.href = checkoutUrl; - ``` - - - - - - - - - Retrieves information about a specific item (such as credits, subscription quantities, etc.) for the user. - - ### Parameters - - - The ID of the item to retrieve. - - - ### Returns - - `Promise`: The item object containing display name, quantity, and other details. - - - - - ```typescript - declare function getItem(itemId: string): Promise; - ``` - - - ```typescript Getting user credits - const credits = await user.getItem("credits"); - console.log(`User has ${credits.quantity} credits`); - console.log(`Non-negative quantity: ${credits.nonNegativeQuantity}`); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Retrieves information about a specific item for the user, used as a React hook. - - ### Parameters - - - The ID of the item to retrieve. - - - ### Returns - - `Item`: The item object containing display name, quantity, and other details. - - - - - ```typescript - declare function useItem(itemId: string): Item; - ``` - - - ```typescript Using credits in a React component - function CreditsDisplay() { - const user = useUser(); - const credits = user.useItem("credits"); - - return ( -
-

Available Credits: {credits.quantity}

-

Display Name: {credits.displayName}

-
- ); - } - ``` -
-
-
-
-{/* END_PLATFORM */} + --- -
- # `ServerUser` -The `ServerUser` object contains most `CurrentUser` properties and methods with the exception of those that require an active session (`getAuthJson` and `signOut`). It also contains some additional functions that require [server-level permissions](/docs/concepts/stack-app#client-vs-server). - -### Table of Contents - - //$stack-link-to:#currentuser - & { - lastActiveAt: Date; //$stack-link-to:#serveruserlastactiveat - serverMetadata: Json; //$stack-link-to:#serveruserservermetadata - - update(data): Promise; //$stack-link-to:#serveruserupdate - - listContactChannels(): Promise; //$stack-link-to:#serveruserlistcontactchannels - // NEXT_LINE_PLATFORM react-like - ⤷ useContactChannels(): ContactChannel[]; //$stack-link-to:#serveruserusecontactchannels - - grantPermission(scope, permissionId): Promise; //$stack-link-to:#serverusergrantpermission - revokePermission(scope, permissionId): Promise; //$stack-link-to:#serveruserrevokepermission - };`} /> - - - - - The last active date and time of the user as a `Date`. - - - - ```typescript - declare const lastActiveAt: Date; - ``` - - - - - - - - The server metadata of the user, accessible only on the server side. - - - - ```typescript - declare const serverMetadata: Json; - ``` - - - - - - - - Updates the user's information on the server side. This is similar to the `CurrentUser.update()` method but includes additional capabilities, such as updating server metadata or setting a new password directly. - - ### Parameters - - - The fields to update. - - - The new display name for the user. - - - The new primary email for the user. - - - Whether the primary email is verified. - - - Whether auth should be enabled for the primary email. - - - The new password for the user. - - - The ID of the team to set as selected, or `null` to clear selection. - - - The URL of the user's new profile image, or `null` to remove. - - - Metadata visible on the client side. - - - Metadata that is read-only on the client but modifiable on the server side. - - - Metadata only accessible and modifiable on the server side. - - - - - ### Returns - - `Promise` - - - - - ```typescript - declare function update(data: { - displayName?: string; - profileImageUrl?: string | null; - primaryEmail?: string, - primaryEmailVerified?: boolean, - primaryEmailAuthEnabled?: boolean, - password?: string; - selectedTeamId?: string | null; - clientMetadata?: Json; - clientReadOnlyMetadata?: Json; - serverMetadata?: Json; - }): Promise; - ``` - - - ```typescript Updating user details on the server - await serverUser.update({ - displayName: "Updated Display Name", - password: "newSecurePassword", - serverMetadata: { - internalNote: "Confidential information", - }, - }); - ``` - - - - - - - - - Lists all the contact channels of the user on the server side. This is similar to `CurrentUser.listContactChannels()` but returns a list of `ServerContactChannel` objects, which may include additional server-only details. - - ### Parameters - - No parameters. - - ### Returns +Like `CurrentUser`, but includes additional methods and properties that require the `SECRET_SERVER_KEY`. - `Promise`: An array of server-specific contact channels. - - - - - ```typescript - declare function listContactChannels(): Promise; - ``` - - - ```typescript Listing server-specific contact channels - const contactChannels = await serverUser.listContactChannels(); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - - - Functionally equivalent to [`listContactChannels()`](#serveruserlistcontactchannels), but as a React hook. - - - - ```typescript - declare function useContactChannels(): ContactChannel[]; - ``` - - - -{/* END_PLATFORM */} - - - - - Grants a specific permission to the user for a given team. - - ### Parameters - - - The team to grant the permission for. - - - The ID of the permission to grant. - - - ### Returns - - `Promise` - - - - - ```typescript - declare function grantPermission(scope: Team, permissionId: string): Promise; - ``` - - - ```typescript Granting permission to a user - await serverUser.grantPermission(team, "read_secret_info"); - ``` - - - - - - - - - Revokes a specific permission from the user for a given team. - - ### Parameters - - - The team to revoke the permission from. - - - The ID of the permission to revoke. - - - ### Returns - - `Promise` - - - - - ```typescript - declare function revokePermission(scope: Team, permissionId: string): Promise; - ``` - - - ```typescript Revoking permission from a user - await serverUser.revokePermission(team, "read_secret_info"); - ``` - - - - - - - - - Creates a new API key for the user, which can be used for programmatic access to your application's backend. - - ### Parameters - - - Options for creating the API key. - - - A human-readable description of the API key's purpose. - - - The date when the API key will expire. Use null for keys that don't expire. - - - Whether the API key is public. Defaults to false. - - - **Secret API Keys** (default) begin with `sk_` and are monitored by Stack Auth's secret scanner, which can revoke them if detected in public code repositories. - - **Public API Keys** begin with `pk_` and are designed for client-side code where exposure is not a concern. - - - - - ### Returns - - `Promise`: The newly created API key. Note that this is the only time the full API key value will be visible. - - - - - ```typescript - declare function createApiKey(options: { - description: string; - expiresAt: Date | null; - isPublic?: boolean; - }): Promise; - ``` - - - ```typescript Creating an API key - const apiKey = await serverUser.createApiKey({ - description: "Backend integration", - expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days - isPublic: false, - }); - - // Save the API key value securely, as it won't be retrievable later - console.log("API Key:", apiKey.value); - ``` - - - - - - - - - Lists all API keys that belong to the user. - - ### Parameters - - None. - - ### Returns - - `Promise`: An array of API keys belonging to the user. - - - - - ```typescript - declare function listApiKeys(): Promise; - ``` - - - ```typescript Listing API keys - const apiKeys = await serverUser.listApiKeys(); - console.log(`You have ${apiKeys.length} API keys`); - - // Find keys that are about to expire - const soonToExpire = apiKeys.filter(key => - key.expiresAt && key.expiresAt.getTime() - Date.now() < 7 * 24 * 60 * 60 * 1000 - ); - ``` - - - - - -{/* IF_PLATFORM next */} - - - - Lists all API keys that belong to the user, used as a React hook. - - ### Parameters - - None. - - ### Returns - - `UserApiKey[]`: An array of API keys belonging to the user. - - - - - ```typescript - declare function useApiKeys(): UserApiKey[]; - ``` - - - ```typescript Using API keys in a React component - function ApiKeysList() { - const user = useUser(); - const apiKeys = user.useApiKeys(); - - return ( -
-

Your API Keys ({apiKeys.length})

-
    - {apiKeys.map(key => ( -
  • - {key.description} - Last four: {key.value.lastFour} - {key.isValid() ? ' (valid)' : ` (invalid: ${key.whyInvalid()})`} -
  • - ))} -
-
- ); - } - ``` -
-
-
-
-{/* END_PLATFORM */} + --- -
- # `CurrentServerUser` -The `CurrentServerUser` object combines all the properties and methods of both `CurrentUser` and `ServerUser`. You can obtain a `CurrentServerUser` by calling `stackServerApp.getUser()` on the server side. - -### Table of Contents +Combines both `CurrentUser` and `ServerUser` - has both session authentication and server-side capabilities. - + diff --git a/docs/src/components/mdx/base-codeblock.tsx b/docs/src/components/mdx/base-codeblock.tsx index 86db92325e..829913ffdd 100644 --- a/docs/src/components/mdx/base-codeblock.tsx +++ b/docs/src/components/mdx/base-codeblock.tsx @@ -1,8 +1,8 @@ 'use client'; import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises'; -import { Check, Copy } from 'lucide-react'; -import { useEffect, useState, type ReactNode } from 'react'; +import { Check, ChevronDown, Copy } from 'lucide-react'; +import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'; import { codeToHtml } from 'shiki'; import { cn } from '../../lib/cn'; @@ -50,6 +50,35 @@ export function BaseCodeblock({ const [highlightedCode, setHighlightedCode] = useState(''); const [isClient, setIsClient] = useState(false); const [copied, setCopied] = useState(false); + const [canScrollDown, setCanScrollDown] = useState(false); + const scrollContainerRef = useRef(null); + + // Check if there's more content to scroll + const checkScrollability = useCallback(() => { + const container = scrollContainerRef.current; + if (container) { + const hasMoreContent = container.scrollHeight > container.clientHeight; + const isNearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 20; + setCanScrollDown(hasMoreContent && !isNearBottom); + } + }, []); + + // Set up scroll listener + useEffect(() => { + const container = scrollContainerRef.current; + if (container) { + checkScrollability(); + container.addEventListener('scroll', checkScrollability); + // Also check on resize + const resizeObserver = new ResizeObserver(checkScrollability); + resizeObserver.observe(container); + + return () => { + container.removeEventListener('scroll', checkScrollability); + resizeObserver.disconnect(); + }; + } + }, [checkScrollability, highlightedCode]); const handleCopy = async () => { try { @@ -179,7 +208,10 @@ export function BaseCodeblock({ {copied ? : } -
+
{children}
+ + {/* Scroll indicator with fade gradient - positioned outside scroll container */} + {canScrollDown && ( +
+ {/* Fade gradient */} +
+ + {/* Scroll indicator */} +
+ + Scroll + +
+
+ )}
diff --git a/docs/src/components/mdx/clickable-code-styles.css b/docs/src/components/mdx/clickable-code-styles.css index ff5a8cd822..7898471394 100644 --- a/docs/src/components/mdx/clickable-code-styles.css +++ b/docs/src/components/mdx/clickable-code-styles.css @@ -5,6 +5,12 @@ font-size: 0.875rem !important; } +/* Ensure scroll container is scrollable */ +.clickable-code-container [class*="overflow-auto"] { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; +} + .clickable-code-container pre, .clickable-code-container code { font-family: inherit !important; diff --git a/docs/src/components/mdx/platform-codeblock.tsx b/docs/src/components/mdx/platform-codeblock.tsx index 41fd7029d5..591031d2eb 100644 --- a/docs/src/components/mdx/platform-codeblock.tsx +++ b/docs/src/components/mdx/platform-codeblock.tsx @@ -284,6 +284,7 @@ export function PlatformCodeblock({ // ALL useState HOOKS MUST BE AT THE TOP const [selectedPlatform, setSelectedPlatform] = useState(getInitialPlatform); + const [pendingPlatform, setPendingPlatform] = useState(null); const [selectedFrameworks, setSelectedFrameworks] = useState<{ [platform: string]: string }>(() => { // Initialize with defaults from config to ensure SSR/client hydration match const initial: { [platform: string]: string } = {}; @@ -370,9 +371,10 @@ export function PlatformCodeblock({ return Math.abs(hash).toString(36).slice(0, 9); }, [documentPath, exampleNames]); - // Get current framework options for selected platform - const currentFrameworks = Object.keys(platforms[selectedPlatform] ?? {}); - const currentFramework = selectedFrameworks[selectedPlatform] || currentFrameworks[0]; + // Get current framework options for selected (or pending) platform + const activePlatform = pendingPlatform || selectedPlatform; + const currentFrameworks = Object.keys(platforms[activePlatform] ?? {}); + const currentFramework = selectedFrameworks[activePlatform] || currentFrameworks[0]; // Helper functions for variants (supports dynamic variant names like 'server'/'client' or 'html'/'script') const getVariantKeys = (platform: string, framework: string): string[] => { @@ -494,6 +496,7 @@ export function PlatformCodeblock({ if (!target.closest(`[data-dropdown-id="${componentId}"]`)) { setIsDropdownOpen(false); setDropdownView('platform'); + setPendingPlatform(null); // Clear pending selection } }; @@ -505,12 +508,23 @@ export function PlatformCodeblock({ const handlePlatformSelect = (platform: string) => { - broadcastPlatformChange(platform); + // Store pending platform but don't apply it yet + setPendingPlatform(platform); setDropdownView('framework'); }; const handleFrameworkSelect = (framework: string) => { - broadcastFrameworkChange(selectedPlatform, framework); + // Use pending platform if available, otherwise current platform + const platformToUse = pendingPlatform || selectedPlatform; + + // Now apply the platform change + setSelectedPlatform(platformToUse); + setPendingPlatform(null); + + // Broadcast both platform and framework changes + broadcastPlatformChange(platformToUse); + broadcastFrameworkChange(platformToUse, framework); + setIsDropdownOpen(false); setDropdownView('platform'); }; @@ -520,6 +534,9 @@ export function PlatformCodeblock({ const next = !prev; if (next) { setDropdownView('platform'); + } else { + // Closing dropdown - clear pending platform if any + setPendingPlatform(null); } return next; }); @@ -641,7 +658,9 @@ export function PlatformCodeblock({ ) } beforeCodeContent={ - hasVariants(selectedPlatform, currentFramework) ? ( + // Use selectedPlatform (not pending) to determine if variants should show + // This ensures variant buttons don't disappear during platform selection + hasVariants(selectedPlatform, selectedFrameworks[selectedPlatform] || Object.keys(platforms[selectedPlatform] ?? {})[0]) ? (
{getVariantKeys(selectedPlatform, currentFramework).map((variant) => { diff --git a/docs/src/components/mdx/sdk-components.tsx b/docs/src/components/mdx/sdk-components.tsx index 30be8a8a7f..57c296b886 100644 --- a/docs/src/components/mdx/sdk-components.tsx +++ b/docs/src/components/mdx/sdk-components.tsx @@ -122,22 +122,30 @@ function ClickableCodeblock({ const topPosition = linePosition.top; // No extra padding needed now const height = linePosition.height; + const isSafeAnchor = area.anchor.startsWith('#') && /^#[a-z0-9-]+$/i.test(area.anchor); + if (!isSafeAnchor) return null; + return (
- -
+ aria-label={`Navigate to ${area.anchor}`} + onClick={() => { + window.location.hash = area.anchor; + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + window.location.hash = area.anchor; + } + }} + /> ); })}
diff --git a/docs/src/components/sdk/hook-documentation.tsx b/docs/src/components/sdk/hook-documentation.tsx new file mode 100644 index 0000000000..35d7f31875 --- /dev/null +++ b/docs/src/components/sdk/hook-documentation.tsx @@ -0,0 +1,515 @@ +'use client'; + +import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises'; +import React, { useEffect, useState } from 'react'; +import { codeToHtml } from 'shiki'; +import { ParamField } from '../mdx/sdk-components'; +import { AsideSection, CollapsibleMethodSection, MethodAside, MethodContent, MethodLayout } from '../ui/method-layout'; + +// Type definitions based on hooks.json structure +type HookInfo = { + name: string, + kind: 'function', + sourcePath: string, + line: number, + category: 'hooks', + type: string, + declaration: string, + description?: string, + signatures?: string[], + tags?: Array<{ + name: string, + text: string, + }>, +}; + +type HookDocumentationProps = { + hookInfo: HookInfo, +}; + +// Syntax highlighted code block component +function SyntaxHighlightedCode({ code, language = 'typescript' }: { code: string, language?: string }) { + const [highlightedCode, setHighlightedCode] = useState(''); + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + useEffect(() => { + if (!isClient) return; + + const updateHighlightedCode = async () => { + const isDarkMode = document.documentElement.classList.contains('dark') || + getComputedStyle(document.documentElement).getPropertyValue('--fd-background').includes('0 0% 3.9%'); + const theme = isDarkMode ? 'github-dark' : 'github-light'; + + const html = await codeToHtml(code, { + lang: language, + theme, + transformers: [{ + pre(node) { + if (node.properties.style) { + node.properties.style = (node.properties.style as string).replace(/background[^;]*;?/g, ''); + } + }, + code(node) { + if (node.properties.style) { + node.properties.style = (node.properties.style as string).replace(/background[^;]*;?/g, ''); + } + } + }] + }); + setHighlightedCode(html); + }; + + const onError = () => setHighlightedCode(''); + + runAsynchronously(updateHighlightedCode, { onError }); + + // Listen for theme changes + const observer = new MutationObserver(() => { + runAsynchronously(updateHighlightedCode, { onError }); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'] + }); + + return () => observer.disconnect(); + }, [code, language, isClient]); + + if (!highlightedCode) { + return
{code}
; + } + + return
; +} + +// Get a human-readable description for a parameter +function getParameterDescription(hookName: string, paramName: string, paramType: string): string { + if (hookName === 'useUser' && paramName === 'options') { + return 'Configuration options for the hook. Use `or: "redirect"` to redirect if not logged in, or `or: "throw"` to throw an error.'; + } + if (hookName === 'useStackApp' && paramName === 'options') { + return 'Configuration options. Use `projectIdMustMatch` to validate the project ID.'; + } + return `Parameter of type ${paramType}.`; +} + +// Clean return type - remove internal types from display +function cleanReturnType(hookName: string, returnType: string): string { + const cleaned = cleanTypeString(returnType); + + // For useUser, simplify to just CurrentUser | null + if (hookName === 'useUser') { + if (cleaned.includes('Internal')) { + return 'CurrentUser | null'; + } + } + + return cleaned; +} + +// Clean up type strings - remove import paths, simplify types +function cleanTypeString(type: string): string { + return type + // Remove import(...) paths + .replace(/import\([^)]+\)\./g, '') + // Simplify RequestLike to just RequestLike + .replace(/RequestLike \| \{ accessToken: string; refreshToken: string; \}/g, 'RequestLike') + // Clean up whitespace + .replace(/\s+/g, ' ') + .trim(); +} + +// Simplify a complex options type to something more readable +function simplifyOptionsType(optionsType: string): string { + const cleaned = cleanTypeString(optionsType); + + // If it's a GetUserOptions reference, use that + if (cleaned.includes('GetUserOptions')) { + return 'GetUserOptions'; + } + + // For complex intersection types, extract the key differentiating parts + // Like { or: "redirect" | "throw"; projectIdMustMatch: "internal"; } + const orMatch = cleaned.match(/or:\s*("[^"]+"\s*\|\s*"[^"]+"|\\"[^"]+\\")/); + const projectMatch = cleaned.match(/projectIdMustMatch:\s*("[^"]+"|string)/); + + if (orMatch || projectMatch) { + const parts: string[] = []; + if (orMatch) { + parts.push(`or: ${orMatch[1].replace(/\\"/g, '"')}`); + } + if (projectMatch) { + parts.push(`projectIdMustMatch: ${projectMatch[1].replace(/\\"/g, '"')}`); + } + return `{ ${parts.join('; ')}; ... }`; + } + + // If still too long, truncate with an ellipsis object + if (cleaned.length > 60) { + return '{ ... }'; + } + + return cleaned; +} + +// Parse a signature handling nested braces/parens +function parseSignatureComponents(signature: string): { params: string, returnType: string } | null { + const cleaned = cleanTypeString(signature); + + let parenDepth = 0; + let braceDepth = 0; + let paramStart = -1; + let paramEnd = -1; + + for (let i = 0; i < cleaned.length; i++) { + const char = cleaned[i]; + if (char === '(') { + if (parenDepth === 0 && braceDepth === 0) paramStart = i; + parenDepth++; + } else if (char === ')') { + parenDepth--; + if (parenDepth === 0 && braceDepth === 0) { + paramEnd = i; + break; + } + } else if (char === '{') { + braceDepth++; + } else if (char === '}') { + braceDepth--; + } + } + + if (paramStart === -1 || paramEnd === -1) return null; + + const params = cleaned.slice(paramStart + 1, paramEnd); + const rest = cleaned.slice(paramEnd + 1); + const returnMatch = rest.match(/\s*=>\s*(.+)$/); + if (!returnMatch) return null; + + return { params, returnType: returnMatch[1].trim() }; +} + +// Format multiple signatures for display +function formatHookSignatures(hookName: string, signatures: string[]): string { + // For hooks with simple signatures (1 signature or short), show full signature + if (signatures.length === 1) { + return formatSingleSignature(hookName, signatures[0]); + } + + // For overloaded hooks, find the most general/public signature + // Skip internal types - we don't document those + for (const sig of signatures) { + const parsed = parseSignatureComponents(sig); + if (parsed) { + // Skip signatures with Internal types + if (parsed.returnType.includes('Internal')) continue; + + // Prefer the most general signature (with union return type or null) + if (parsed.returnType.includes('null') || parsed.returnType.includes('|')) { + const params = parsed.params.trim() ? simplifyOptionsType(parsed.params) : ''; + const paramStr = params ? `options?: ${params}` : ''; + return `declare function ${hookName}(${paramStr}): ${parsed.returnType};`; + } + } + } + + // Fallback: find any non-internal signature + for (const sig of signatures) { + const parsed = parseSignatureComponents(sig); + if (parsed && !parsed.returnType.includes('Internal')) { + const params = parsed.params.trim() ? simplifyOptionsType(parsed.params) : ''; + const paramStr = params ? `options?: ${params}` : ''; + return `declare function ${hookName}(${paramStr}): ${parsed.returnType};`; + } + } + + // Last resort: use first signature + return formatSingleSignature(hookName, signatures[0]); +} + +// Format a single hook signature nicely for display +function formatSingleSignature(hookName: string, signature: string): string { + const cleaned = cleanTypeString(signature); + + // Try to parse function signature: (params) => ReturnType + const genericMatch = cleaned.match(/^(<[^>]+>)/); + const generics = genericMatch ? genericMatch[1] : ''; + + const restOfSig = genericMatch ? cleaned.slice(generics.length) : cleaned; + const match = restOfSig.match(/\((.*?)\)\s*=>\s*(.+)$/s); + + if (!match) { + return `declare function ${hookName}${cleaned}`; + } + + const params = match[1]; + const returnType = match[2]; + + if (!params.trim()) { + return `declare function ${hookName}${generics}(): ${returnType};`; + } + + // Parse parameter name and type + const paramMatch = params.match(/^(\w+)\??:\s*(.+)$/); + if (paramMatch) { + const paramName = paramMatch[1]; + const paramType = paramMatch[2]; + const isOptional = params.includes('?'); + return `declare function ${hookName}${generics}(\n ${paramName}${isOptional ? '?' : ''}: ${paramType}\n): ${returnType};`; + } + + return `declare function ${hookName}${generics}(${params}): ${returnType};`; +} + +function parseSignature(signature: string): { + parameters: Array<{ name: string, type: string, optional: boolean, description?: string }>, + returnType: string, +} | null { + const cleaned = cleanTypeString(signature); + + // Try to parse function signature: (params) => ReturnType + // Need to handle nested braces in the params + let parenDepth = 0; + let braceDepth = 0; + let paramStart = -1; + let paramEnd = -1; + + for (let i = 0; i < cleaned.length; i++) { + const char = cleaned[i]; + if (char === '(') { + if (parenDepth === 0 && braceDepth === 0) paramStart = i; + parenDepth++; + } else if (char === ')') { + parenDepth--; + if (parenDepth === 0 && braceDepth === 0) { + paramEnd = i; + break; + } + } else if (char === '{') { + braceDepth++; + } else if (char === '}') { + braceDepth--; + } + } + + if (paramStart === -1 || paramEnd === -1) return null; + + const paramsStr = cleaned.slice(paramStart + 1, paramEnd); + const rest = cleaned.slice(paramEnd + 1); + const returnMatch = rest.match(/\s*=>\s*(.+)$/); + if (!returnMatch) return null; + + const returnType = returnMatch[1].trim(); + const parameters: Array<{ name: string, type: string, optional: boolean, description?: string }> = []; + + if (paramsStr.trim()) { + // Parse param: name?: type + const paramMatch = paramsStr.match(/^(\w+)(\?)?:\s*(.+)$/); + if (paramMatch) { + const name = paramMatch[1]; + const optional = !!paramMatch[2]; + const type = paramMatch[3]; + parameters.push({ name, type, optional }); + } + } + + return { parameters, returnType }; +} + +export function HookDocumentation({ hookInfo }: HookDocumentationProps) { + const primarySignature = hookInfo.signatures?.[0]; + const parsedSignature = primarySignature ? parseSignature(primarySignature) : null; + + return ( +
+ {/* Hook Header */} +
+

+ {hookInfo.name} +

+ + {hookInfo.description && ( +

+ {hookInfo.description} +

+ )} + +
+
+ Source:{' '} + {hookInfo.sourcePath} +
+
+ Line:{' '} + {hookInfo.line} +
+
+
+ + {/* Hook Usage */} + p.name).join(', ')} + appType="StackClientApp" + defaultOpen={true} + > + + + {hookInfo.tags?.some(tag => tag.name === 'deprecated') && ( +
+
+ ⚠️ Deprecated +
+
+ {hookInfo.tags.find(tag => tag.name === 'deprecated')?.text || 'This hook is deprecated.'} +
+
+ )} + +

Parameters

+ + {(!parsedSignature || parsedSignature.parameters.length === 0) ? ( +

No parameters.

+ ) : ( +
+ {parsedSignature.parameters.map((param, index) => { + const simplifiedType = simplifyOptionsType(param.type); + return ( + + {getParameterDescription(hookInfo.name, param.name, simplifiedType)} + + ); + })} +
+ )} + +

Returns

+

+ + {cleanReturnType(hookInfo.name, parsedSignature?.returnType ?? 'unknown')} + +

+
+ + + + + + + {hookInfo.sourcePath && hookInfo.line ? ( +
+ ) : null} + + + +
+ ); +} + +// Component to load and display a specific hook from hooks.json +export function HookFromJson({ hookName }: { hookName: string }) { + const [hookInfo, setHookInfo] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + const controller = new AbortController(); + + async function loadHookInfo() { + setLoading(true); + setError(null); + + try { + const response = await fetch('/sdk-docs/hooks.json', { signal: controller.signal }); + if (!response.ok) { + throw new Error(`Failed to load hooks.json: ${response.statusText}`); + } + + const hooksData: Partial> = await response.json(); + const foundHook = hooksData[hookName]; + + if (!foundHook) { + const availableKeys = Object.keys(hooksData); + const preview = availableKeys.slice(0, 10).join(', '); + const suffix = availableKeys.length > 10 ? `, ... (${availableKeys.length} total)` : ''; + throw new Error(`Hook "${hookName}" not found in hooks.json. Available hooks: ${preview}${suffix}`); + } + + if (!controller.signal.aborted) { + setHookInfo(foundHook); + } + } catch (err) { + if (controller.signal.aborted) return; + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + if (!controller.signal.aborted) { + setLoading(false); + } + } + } + runAsynchronously(loadHookInfo()); + + return () => controller.abort(); + }, [hookName]); + + if (loading) { + return ( +
+
Loading hook documentation...
+
+ ); + } + + if (error) { + return ( +
+
Error Loading Hook
+
{error}
+
+ ); + } + + if (!hookInfo) { + return ( +
+ Hook not found. +
+ ); + } + + return ; +} + diff --git a/docs/src/components/sdk/object-documentation.tsx b/docs/src/components/sdk/object-documentation.tsx new file mode 100644 index 0000000000..621e001d34 --- /dev/null +++ b/docs/src/components/sdk/object-documentation.tsx @@ -0,0 +1,229 @@ +'use client'; + +import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises'; +import React from 'react'; +import { AsideSection, CollapsibleMethodSection, MethodAside, MethodContent, MethodLayout } from '../ui/method-layout'; + +// Type definitions based on objects.json structure +type ObjectInfo = { + name: string, + kind: 'variable' | 'type' | 'class', + sourcePath: string, + line: number, + category: 'objects', + type: string, + declaration: string, + description?: string, + signatures?: string[], + tags?: Array<{ + name: string, + text: string, + }>, +}; + +type ObjectDocumentationProps = { + objectInfo: ObjectInfo, +}; + +function formatTypeSignature(type: string): string { + // Clean up long import paths and make types more readable + return type + .replace(/import\([^)]+\)\./g, '') // Remove import() paths + .replace(/\s+/g, ' ') // Normalize whitespace + .trim(); +} + +export function ObjectDocumentation({ objectInfo }: ObjectDocumentationProps) { + return ( +
+ {/* Object Header */} +
+

+ {objectInfo.name} +

+ + {objectInfo.description && ( +

+ {objectInfo.description} +

+ )} + +
+
+ Kind:{' '} + {objectInfo.kind} +
+
+ Source:{' '} + {objectInfo.sourcePath} +
+
+ Line:{' '} + {objectInfo.line} +
+
+
+ + {/* Object Definition */} + + + + {objectInfo.tags?.some(tag => tag.name === 'deprecated') && ( +
+
+ ⚠️ Deprecated +
+
+ {objectInfo.tags.find(tag => tag.name === 'deprecated')?.text || 'This object is deprecated.'} +
+
+ )} + +

Type

+

+ + {formatTypeSignature(objectInfo.type)} + +

+ + {objectInfo.signatures && objectInfo.signatures.length > 0 && ( + <> +

Call Signatures

+
+ {objectInfo.signatures.map((sig, index) => ( +
+ {formatTypeSignature(sig)} +
+ ))} +
+ + )} + + {objectInfo.tags && objectInfo.tags.length > 0 && ( + <> +

Additional Information

+
+ {objectInfo.tags.filter(tag => tag.name !== 'deprecated').map((tag, index) => ( +
+ @{tag.name}:{' '} + {tag.text} +
+ ))} +
+ + )} +
+ + + +
+                {objectInfo.declaration}
+              
+
+ + {objectInfo.signatures && objectInfo.signatures.length > 0 && ( + +
+                  
+                    {objectInfo.signatures.map((sig, index) => (
+                      
+                        {sig}
+                      
+                    ))}
+                  
+                
+
+ )} + + +
+
File: {objectInfo.sourcePath}
+
Line: {objectInfo.line}
+
+
+
+
+
+
+ ); +} + +// Component to load and display a specific object from objects.json +export function ObjectFromJson({ objectName }: { objectName: string }) { + const [objectInfo, setObjectInfo] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + const controller = new AbortController(); + + async function loadObjectInfo() { + setLoading(true); + setError(null); + + try { + const response = await fetch('/sdk-docs/objects.json', { signal: controller.signal }); + if (!response.ok) { + throw new Error(`Failed to load objects.json: ${response.statusText}`); + } + + const objectsData: Partial> = await response.json(); + const foundObject = objectsData[objectName]; + + if (!foundObject) { + const availableKeys = Object.keys(objectsData); + const preview = availableKeys.slice(0, 10).join(', '); + const suffix = availableKeys.length > 10 ? `, ... (${availableKeys.length} total)` : ''; + throw new Error(`Object "${objectName}" not found in objects.json. Available objects: ${preview}${suffix}`); + } + + if (!controller.signal.aborted) { + setObjectInfo(foundObject); + } + } catch (err) { + if (controller.signal.aborted) return; + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + if (!controller.signal.aborted) { + setLoading(false); + } + } + } + runAsynchronously(loadObjectInfo()); + + return () => controller.abort(); + }, [objectName]); + + if (loading) { + return ( +
+
Loading object documentation...
+
+ ); + } + + if (error) { + return ( +
+
Error Loading Object
+
{error}
+
+ ); + } + + if (!objectInfo) { + return ( +
+ Object not found. +
+ ); + } + + return ; +} + diff --git a/docs/src/components/sdk/overview.tsx b/docs/src/components/sdk/overview.tsx index 57b153b98d..c973ceff8f 100644 --- a/docs/src/components/sdk/overview.tsx +++ b/docs/src/components/sdk/overview.tsx @@ -79,8 +79,8 @@ export function SDKOverview({ sections }: SDKOverviewProps) { key={itemIndex} href={buildSDKUrl(item.href)} className={cn( - 'group relative flex items-center gap-3 p-4 rounded-lg border transition-all duration-200', - 'hover:shadow-md hover:shadow-fd-primary/5 hover:border-fd-primary/20', + 'group relative flex items-center gap-3 p-4 rounded-lg border transition-colors hover:transition-none', + 'hover:border-fd-primary/20', 'bg-fd-card/30 hover:bg-fd-card/50', 'border-fd-border hover:border-fd-primary/30' )} diff --git a/docs/src/components/sdk/type-documentation.tsx b/docs/src/components/sdk/type-documentation.tsx index c465dcc6bf..0d589fd29f 100644 --- a/docs/src/components/sdk/type-documentation.tsx +++ b/docs/src/components/sdk/type-documentation.tsx @@ -1,8 +1,11 @@ 'use client'; import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises'; -import React from 'react'; -import { ClickableTableOfContents, ParamField } from '../mdx/sdk-components'; +import React, { useEffect, useState } from 'react'; +import { codeToHtml } from 'shiki'; +import { cn } from '../../lib/cn'; +import { Info } from '../mdx/info'; +import { Accordion, ClickableTableOfContents, ParamField } from '../mdx/sdk-components'; import { AsideSection, CollapsibleTypesSection, MethodAside, MethodContent, MethodLayout } from '../ui/method-layout'; // Type definitions based on the types.json structure @@ -20,6 +23,11 @@ type TypeMember = { name: string, type: string, optional: boolean, + propertyDescriptions?: Record, }>, returnType: string, }>, @@ -40,19 +48,305 @@ type TypeInfo = { description?: string, members: TypeMember[], mixins?: string[], + ownMemberNames?: string[], }; type TypeDocumentationProps = { typeInfo: TypeInfo, platform?: string, + parentTypes?: string[], }; function formatTypeSignature(type: string): string { // Clean up long import paths and make types more readable - return type + let cleaned = type .replace(/import\([^)]+\)\./g, '') // Remove import() paths .replace(/\s+/g, ' ') // Normalize whitespace .trim(); + + // Simplify long union types of string literals (e.g., "a" | "b" | "c" | ... -> string) + const stringLiteralUnionMatch = cleaned.match(/^"[\w-]+"(\s*\|\s*"[\w-]+")+$/); + if (stringLiteralUnionMatch) { + const options = cleaned.split('|').map(s => s.trim().replace(/"/g, '')); + if (options.length > 4) { + // For long union types, show a simplified version + return `string (one of: ${options.slice(0, 3).join(', ')}, ... +${options.length - 3} more)`; + } + } + + // Simplify complex conditional/union types (too complex to display inline) + if (cleaned.includes(' extends ') && cleaned.length > 100) { + return 'object (see signature for details)'; + } + + return cleaned; +} + +// Format return type, keeping it concise for display +function formatReturnType(returnType: string): string { + let cleaned = formatTypeSignature(returnType); + + // Simplify very long object types in Promise<...> + // Match Promise<{ ... many properties ... }[]> or Promise<{ ... }> + const promiseMatch = cleaned.match(/^Promise<(\{.+\})(\[\])?>/); + if (promiseMatch) { + const objectType = promiseMatch[1]; + const isArray = promiseMatch[2]; + + // If the object type is very long (>150 chars), it's likely an expanded entity type + if (objectType.length > 150) { + // Try to detect what type it might be based on distinctive properties + if (objectType.includes('type: "user"') && objectType.includes('createdAt:') && objectType.includes('description:')) { + return isArray ? 'Promise' : 'Promise'; + } + if (objectType.includes('type: "team"') && objectType.includes('createdAt:') && objectType.includes('description:')) { + return isArray ? 'Promise' : 'Promise'; + } + if (objectType.includes('displayName:') && objectType.includes('primaryEmail:')) { + return isArray ? 'Promise' : 'Promise'; + } + if (objectType.includes('displayName') && objectType.includes('profilePictureUrl')) { + return isArray ? 'Promise' : 'Promise'; + } + if (objectType.includes('permissionId') && objectType.includes('userId')) { + return isArray ? 'Promise' : 'Promise'; + } + if (objectType.includes('value:') && objectType.includes('type:') && objectType.includes('isPrimary')) { + return isArray ? 'Promise' : 'Promise'; + } + // Generic fallback based on distinctive markers + if (objectType.includes('id:') && objectType.includes('createdAt:')) { + return isArray ? 'Promise' : 'Promise'; + } + // Last resort + return isArray ? 'Promise' : 'Promise'; + } + } + + return cleaned; +} + +// Parse object properties from a type string like "{ prop1?: type1; prop2: type2; }" +// Also handles intersection types like "{ a: string } & { b: number }" +function parseObjectProperties(typeString: string): Array<{ name: string, type: string, optional: boolean }> | null { + const allProperties: Array<{ name: string, type: string, optional: boolean }> = []; + + // Strip trailing "| undefined" for optional parameters + let cleanedType = typeString.replace(/\s*\|\s*undefined\s*$/g, '').trim(); + + // Don't try to parse if it doesn't contain curly braces (not an object type) + if (!cleanedType.includes('{') || !cleanedType.includes('}')) { + return null; + } + + // Don't try to parse complex conditional types or unions with conditionals + if (cleanedType.includes(' extends ') || cleanedType.includes('? {')) { + return null; + } + + // Handle intersection types - split by & at the top level + const intersectionParts = splitIntersectionType(cleanedType); + + for (const part of intersectionParts) { + const trimmedPart = part.trim(); + + // Match object type pattern + const objectMatch = trimmedPart.match(/^\{\s*(.+?)\s*;?\s*\}$/s); + if (!objectMatch) continue; + + const propsString = objectMatch[1]; + + // Split by semicolons, handling nested objects and generics + let currentProp = ''; + let depth = 0; + let inGeneric = 0; + + for (let i = 0; i < propsString.length; i++) { + const char = propsString[i]; + + if (char === '<') inGeneric++; + if (char === '>') inGeneric--; + if (char === '{') depth++; + if (char === '}') depth--; + + if (char === ';' && depth === 0 && inGeneric === 0) { + if (currentProp.trim()) { + const prop = parseSingleProperty(currentProp.trim()); + if (prop) allProperties.push(prop); + } + currentProp = ''; + } else { + currentProp += char; + } + } + + // Handle last property (may not have trailing semicolon) + if (currentProp.trim()) { + const prop = parseSingleProperty(currentProp.trim()); + if (prop) allProperties.push(prop); + } + } + + return allProperties.length > 0 ? allProperties : null; +} + +// Split intersection type at top level (e.g., "A & B" -> ["A", "B"]) +function splitIntersectionType(typeString: string): string[] { + const parts: string[] = []; + let current = ''; + let depth = 0; + let inGeneric = 0; + + for (let i = 0; i < typeString.length; i++) { + const char = typeString[i]; + + if (char === '<') inGeneric++; + if (char === '>') inGeneric--; + if (char === '{') depth++; + if (char === '}') depth--; + + if (char === '&' && depth === 0 && inGeneric === 0) { + if (current.trim()) parts.push(current.trim()); + current = ''; + } else { + current += char; + } + } + + if (current.trim()) parts.push(current.trim()); + + return parts.length > 0 ? parts : [typeString]; +} + +function parseSingleProperty(propString: string): { name: string, type: string, optional: boolean } | null { + // Match property pattern: "propName?: type" or "propName: type" + const match = propString.match(/^(\w+)(\??):\s*(.+)$/); + if (!match) return null; + + // Clean up the type by removing "| undefined" + let type = match[3].trim().replace(/\s*\|\s*undefined\s*$/g, '').trim(); + + return { + name: match[1], + optional: match[2] === '?', + type + }; +} + +// Format a method signature nicely for display +function formatMethodSignature( + signature: { + parameters: Array<{ + name: string, + type: string, + optional: boolean, + propertyDescriptions?: Record, + }>, + returnType: string, + }, + methodName: string +): string { + const formattedReturnType = formatReturnType(signature.returnType); + + if (signature.parameters.length === 0) { + if (formattedReturnType.length > 80) { + return `declare function ${methodName}():\n ${formattedReturnType};`; + } + return `declare function ${methodName}(): ${formattedReturnType};`; + } + + const params = signature.parameters.map(param => { + // If we have propertyDescriptions, use them to format inline object + if (param.propertyDescriptions && Object.keys(param.propertyDescriptions).length > 0) { + const props = Object.entries(param.propertyDescriptions).map(([propName, propInfo]) => { + return ` ${propName}${propInfo.optional ? '?' : ''}: ${propInfo.type};`; + }).join('\n'); + return `${param.name}${param.optional ? '?' : ''}: {\n${props}\n}`; + } + + // Try to parse inline expanded type + const properties = parseObjectProperties(param.type); + + if (properties && properties.length > 0) { + const propsFormatted = properties.map(prop => + ` ${prop.name}${prop.optional ? '?' : ''}: ${prop.type};` + ).join('\n'); + + return `${param.name}${param.optional ? '?' : ''}: {\n${propsFormatted}\n}`; + } else { + return `${param.name}${param.optional ? '?' : ''}: ${param.type}`; + } + }).join(', '); + + const oneLine = `declare function ${methodName}(${params}): ${formattedReturnType};`; + if (oneLine.length > 100) { + return `declare function ${methodName}(${params}):\n ${formattedReturnType};`; + } + + return oneLine; +} + +// Syntax highlighted code block component +function SyntaxHighlightedCode({ code, language = 'typescript' }: { code: string, language?: string }) { + const [highlightedCode, setHighlightedCode] = useState(''); + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + useEffect(() => { + if (!isClient) return; + + const updateHighlightedCode = async () => { + try { + const isDarkMode = document.documentElement.classList.contains('dark') || + getComputedStyle(document.documentElement).getPropertyValue('--fd-background').includes('0 0% 3.9%'); + const theme = isDarkMode ? 'github-dark' : 'github-light'; + + const html = await codeToHtml(code, { + lang: language, + theme, + transformers: [{ + pre(node) { + if (node.properties.style) { + node.properties.style = (node.properties.style as string).replace(/background[^;]*;?/g, ''); + } + }, + code(node) { + if (node.properties.style) { + node.properties.style = (node.properties.style as string).replace(/background[^;]*;?/g, ''); + } + } + }] + }); + setHighlightedCode(html); + } catch (error) { + console.error('Error highlighting code:', error); + setHighlightedCode(`
${code}
`); + } + }; + + runAsynchronously(updateHighlightedCode); + + // Listen for theme changes + const observer = new MutationObserver(() => { + runAsynchronously(updateHighlightedCode); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'] + }); + + return () => observer.disconnect(); + }, [code, language, isClient]); + + if (!highlightedCode) { + return
{code}
; + } + + return
; } function buildAnchorId(typeName: string, memberName: string): string { @@ -61,16 +355,82 @@ function buildAnchorId(typeName: string, memberName: string): string { return `#${cleanType}${cleanMember}`; } -function generateTableOfContents(typeInfo: TypeInfo, platform = 'react-like'): string { +// Categorize a member into a logical group for visual separation +function getMemberGroup(member: TypeMember): string { + const name = member.name; + + if (member.kind === 'property') { + // Group session-related properties + if (name.includes('Session') || name.includes('session')) return 'session'; + return 'properties'; + } + + // Method groupings based on name patterns + if (name.includes('Team') || name.includes('team')) return 'team'; + if (name.includes('Auth') || name.includes('Password') || name.includes('password') || + name === 'signOut' || name === 'signIn' || name === 'signUp') return 'auth'; + if (name.includes('ApiKey') || name.includes('apiKey')) return 'apiKeys'; + if (name.includes('ContactChannel') || name.includes('contactChannel')) return 'contactChannels'; + if (name.includes('Invitation') || name.includes('invitation')) return 'invitations'; + if (name.includes('Permission') || name.includes('permission')) return 'permissions'; + if (name.includes('OAuth') || name.includes('oauth') || name.includes('Connected')) return 'oauth'; + if (name.includes('Product') || name.includes('product') || name.includes('Checkout') || name.includes('Item') || name.includes('item')) return 'billing'; + if (name === 'update' || name === 'delete' || name.startsWith('set')) return 'mutations'; + + return 'other'; +} + +function generateTableOfContents(typeInfo: TypeInfo, platform = 'react-like', membersToShow?: TypeMember[], parentTypes?: string[]): string { const lines: string[] = []; + const members = membersToShow || typeInfo.members; + + // Filter platform-specific members upfront + const filteredMembers = members.filter(member => + !member.platforms || member.platforms.includes(platform) + ); + + // Track which members have been processed (for grouping hooks with their async counterparts) + const processed = new Set(); lines.push(`type ${typeInfo.name} = {`); - typeInfo.members.forEach(member => { - // Skip platform-specific members if they don't match current platform - if (member.platforms && !member.platforms.includes(platform)) { - return; + // Add inheritance information if parentTypes are provided + if (parentTypes && parentTypes.length > 0) { + parentTypes.forEach(parentType => { + lines.push(` // Inherits all functionality from ${parentType}`); + // Generate link based on type - for now, same page anchor + const anchor = `#${parentType.toLowerCase()}`; + lines.push(` & ${parentType} //$stack-link-to:${anchor}`); + }); + if (filteredMembers.length > 0) { + lines.push(` & {`); + } else { + // No new members, just close + lines.push('};'); + return lines.join('\n'); } + } + + // Determine indentation based on whether we have parent types + const indent = parentTypes && parentTypes.length > 0 ? ' ' : ' '; + + // Group members by category for visual separation + const groupOrder = ['session', 'properties', 'mutations', 'auth', 'team', 'invitations', 'permissions', 'oauth', 'contactChannels', 'apiKeys', 'billing', 'other']; + const membersByGroup = new Map(); + + for (const group of groupOrder) { + membersByGroup.set(group, []); + } + + for (const member of filteredMembers) { + const group = getMemberGroup(member); + const groupMembers = membersByGroup.get(group) || membersByGroup.get('other')!; + groupMembers.push(member); + } + + // Helper to render a single member + const renderMember = (member: TypeMember) => { + if (processed.has(member.name)) return; const memberName = member.name; const isOptional = member.optional ? '?' : ''; @@ -78,39 +438,136 @@ function generateTableOfContents(typeInfo: TypeInfo, platform = 'react-like'): s if (member.kind === 'property') { const cleanType = formatTypeSignature(member.type || 'unknown'); - lines.push(` ${memberName}${isOptional}: ${cleanType}; //$stack-link-to:${anchorId}`); + lines.push(`${indent}${memberName}${isOptional}: ${cleanType}; //$stack-link-to:${anchorId}`); + processed.add(memberName); } else { - // For methods, show the first signature or a simplified version - const signature = member.signatures?.[0]; + // member.kind === 'method' + // Check if this is a hook (useX) - skip it, will be paired with async counterpart + const isHook = memberName.startsWith('use') && memberName.length > 3; + if (isHook) return; + + // This is an async method - render it and look for its corresponding hook + const signature = member.signatures?.[member.signatures.length - 1]; if (signature) { - const params = signature.parameters.map(p => { - if (p.optional) { - return `${p.name}?`; + const params = signature.parameters.map(p => p.optional ? `${p.name}?` : p.name).join(', '); + const returnType = formatTypeSignature(signature.returnType); + lines.push(`${indent}${memberName}(${params}): ${returnType}; //$stack-link-to:${anchorId}`); + processed.add(memberName); + + // Look for corresponding hook: getX -> useX, listX -> useX (plural) + let hookName = ''; + if (memberName.startsWith('get')) { + hookName = 'use' + memberName.slice(3); + } else if (memberName.startsWith('list')) { + hookName = 'use' + memberName.slice(4); + } + + const hookMember = filteredMembers.find(m => m.name === hookName); + const isReactHook = hookMember && (!hookMember.platforms || hookMember.platforms.includes('react-like')); + if (hookMember && isReactHook) { + const hookAnchorId = buildAnchorId(typeInfo.name, hookName); + const hookSig = hookMember.signatures?.[hookMember.signatures.length - 1]; + if (hookSig) { + const hookParams = hookSig.parameters.map(p => p.optional ? `${p.name}?` : p.name).join(', '); + const hookReturn = formatTypeSignature(hookSig.returnType); + lines.push(`${indent}// NEXT_LINE_PLATFORM react-like`); + lines.push(`${indent}⤷ ${hookName}(${hookParams}): ${hookReturn}; //$stack-link-to:${hookAnchorId}`); + processed.add(hookName); } - return p.name; - }).join(', '); + } + } + } + }; + + // Render members by group with blank lines between groups + let isFirstGroup = true; + for (const group of groupOrder) { + const groupMembers = membersByGroup.get(group) || []; + if (groupMembers.length === 0) continue; + + // Add blank line between groups (but not before first group) + if (!isFirstGroup) { + lines.push(''); + } + isFirstGroup = false; + + for (const member of groupMembers) { + renderMember(member); + } + } + + // Add any remaining hooks that weren't paired + const unpairedHooks = filteredMembers.filter(m => + !processed.has(m.name) && m.kind === 'method' + ); + + if (unpairedHooks.length > 0) { + lines.push(''); + for (const member of unpairedHooks) { + const memberName = member.name; + const anchorId = buildAnchorId(typeInfo.name, memberName); + const signature = member.signatures?.[member.signatures.length - 1]; + + if (signature) { + const params = signature.parameters.map(p => p.optional ? `${p.name}?` : p.name).join(', '); const returnType = formatTypeSignature(signature.returnType); - lines.push(` ${memberName}(${params}): ${returnType}; //$stack-link-to:${anchorId}`); - } else { - lines.push(` ${memberName}(): unknown; //$stack-link-to:${anchorId}`); + lines.push(`${indent}${memberName}(${params}): ${returnType}; //$stack-link-to:${anchorId}`); + processed.add(memberName); } } - }); + } - lines.push('};'); + // Close the nested object if we have parent types with new members + if (parentTypes && parentTypes.length > 0 && filteredMembers.length > 0) { + lines.push(` };`); + lines.push('};'); + } else if (!parentTypes || parentTypes.length === 0) { + // No parent types, just close normally + lines.push('};'); + } + // If we had parentTypes but no new members, we already closed above return lines.join('\n'); } function renderMemberDocumentation(typeInfo: TypeInfo, member: TypeMember, platform = 'react-like') { const memberName = member.name; - const primarySignature = member.signatures?.[0]; + + // Check if this is a React hook (methods only, not properties like userId) + const isReactHook = member.kind === 'method' && + memberName.startsWith('use') && + memberName.length > 3 && + (!member.platforms || member.platforms.includes('react-like')); + + // For methods with multiple overloads, prefer the first non-tuple signature + // Tuple signatures (args: [...]) are internal representations and less user-friendly + let primarySignature = member.signatures?.[0]; + + // If we have multiple signatures, try to find the most complete non-tuple one + if (member.signatures && member.signatures.length > 1) { + // Filter out tuple signatures + const nonTupleSignatures = member.signatures.filter(sig => + !sig.parameters.some(p => p.type.match(/^\[.+\]$/)) + ); + + if (nonTupleSignatures.length > 0) { + // Use the one with the most parameters (most complete) + primarySignature = nonTupleSignatures.reduce((prev, current) => + current.parameters.length > prev.parameters.length ? current : prev + ); + } else { + // All are tuple signatures, use the last one (most specific for inheritance) + primarySignature = member.signatures[member.signatures.length - 1]; + } + } // Skip platform-specific members if they don't match current platform if (member.platforms && !member.platforms.includes(platform)) { return null; } + const isDeprecated = member.tags?.some(tag => tag.name === 'deprecated') ?? false; + return ( p.name).join(', ') : undefined } + isReactHook={isReactHook} + isDeprecated={isDeprecated} defaultOpen={false} > - {member.description && ( -
- {member.description} -
- )} - {member.tags?.some(tag => tag.name === 'deprecated') && (
⚠️ Deprecated
- {member.tags.find(tag => tag.name === 'deprecated')?.text || 'This method is deprecated.'} + {member.tags.find(tag => tag.name === 'deprecated')?.text || 'This item is deprecated.'} +
+
+ )} + + {member.tags?.some(tag => tag.name === 'note') && ( + + {member.tags.find(tag => tag.name === 'note')?.text} + + )} + + {isReactHook && ( +
+
+ + + + React Hook
+ + Only available in React-based frameworks +
)} +
+ {member.description ? ( + // Parse and render description with proper formatting + // First, extract code blocks (which may contain \n\n), then split the rest + (() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parts: any[] = []; + const description = member.description; + + // Extract code blocks first (they can span multiple paragraphs) + const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g; + let lastIndex = 0; + let match; + + while ((match = codeBlockRegex.exec(description)) !== null) { + // Add text before this code block + if (match.index > lastIndex) { + const textBefore = description.slice(lastIndex, match.index); + if (textBefore.trim()) { + parts.push({ type: 'text', content: textBefore.trim() }); + } + } + // Add the code block + parts.push({ + type: 'code', + content: match[2].trim(), + language: match[1] || 'typescript' + }); + lastIndex = match.index + match[0].length; + } + + // Add any remaining text after the last code block + if (lastIndex < description.length) { + const textAfter = description.slice(lastIndex); + if (textAfter.trim()) { + parts.push({ type: 'text', content: textAfter.trim() }); + } + } + + // If no code blocks found, just split by paragraphs + if (parts.length === 0) { + parts.push({ type: 'text', content: description }); + } + + return parts.map((part, idx) => { + if (part.type === 'code') { + return ( +
+ +
+ ); + } else { + // Split text parts by double newlines for paragraphs + return part.content.split('\n\n').map((paragraph: string, pIdx: number) => ( +

{paragraph}

+ )); + } + }); + })() + ) : ( + `⚠️ Documentation not available for ${memberName}.` + )} +
+ {member.kind === 'method' && ( <> -

Parameters

+

Parameters

{(primarySignature?.parameters.length ?? 0) === 0 ? ( -

No parameters.

+

None.

) : ( -
- {(primarySignature?.parameters ?? []).map((param, index) => ( - - Parameter of type {formatTypeSignature(param.type)}. - - ))} -
- )} + <> + {(primarySignature?.parameters ?? []).map((param, index) => { + const formattedType = formatTypeSignature(param.type); -

Returns

-

- - {formatTypeSignature(primarySignature?.returnType ?? 'unknown')} - -

- - )} + // Check if we have propertyDescriptions (new format) + const hasPropertyInfo = param.propertyDescriptions && Object.keys(param.propertyDescriptions).length > 0; - {member.kind === 'property' && ( - <> -

Type

-

- - {formatTypeSignature(member.type || 'unknown')} - + // Try to parse inline type as fallback + const properties = hasPropertyInfo ? null : parseObjectProperties(param.type); + + return ( + + {hasPropertyInfo ? ( + <> + An object containing properties. + + {Object.entries(param.propertyDescriptions!).map(([propName, propInfo]) => ( + + {propInfo.description || `Property of type ${formatTypeSignature(propInfo.type)}.`} + + ))} + + + ) : properties ? ( + <> + An object containing properties. + + {properties.map((prop, propIndex) => ( + + Property of type {formatTypeSignature(prop.type)}. + + ))} + + + ) : ( + `Parameter of type ${formattedType}.` + )} + + ); + })} + + )} + +

Returns

+

+ {formatReturnType(primarySignature?.returnType ?? 'unknown')}

)}
- {member.kind === 'method' && member.signatures ? ( + {member.kind === 'method' && primarySignature ? ( -
-                
-                  {member.signatures.map((sig, index) => (
-                    
- {sig.signature} -
- ))} -
-
+
) : ( -
-              
-                declare const {memberName}: {formatTypeSignature(member.type || 'unknown')};
-              
-            
+ )} - -
-
File: {member.sourcePath}
-
Line: {member.line}
+ {member.sourcePath && member.line ? ( + + ) : ( +
+
+ Generated property +
- + )} + + ); } -export function TypeDocumentation({ typeInfo, platform = 'react-like' }: TypeDocumentationProps) { - return ( -
- {/* Type Header */} -
-

- {typeInfo.name} -

- - {typeInfo.description && ( -

- {typeInfo.description} -

- )} +// Component to render example code with client/server tabs +function ExampleSection({ tags }: { tags?: Array<{ name: string, text?: string }> }) { + const [activeTab, setActiveTab] = useState<'client' | 'server'>('client'); -
-
- Source:{' '} - {typeInfo.sourcePath} -
-
- Line:{' '} - {typeInfo.line} + const clientExample = tags?.find(t => t.name === 'example-client')?.text; + const serverExample = tags?.find(t => t.name === 'example-server')?.text; + + if (!clientExample && !serverExample) { + return null; + } + + const hasMultipleTabs = clientExample && serverExample; + const currentExample = activeTab === 'client' ? clientExample : serverExample; + + // Extract code from markdown code block if present + const extractCode = (text?: string) => { + if (!text) return ''; + const match = text.match(/```(?:tsx?|js)?\n?([\s\S]*?)```/); + return match ? match[1].trim() : text.trim(); + }; + + return ( +
+
+ Example + {hasMultipleTabs && ( +
+ +
-
+ )}
+ +
+ ); +} - {/* Type Definition */} -
-

Type Definition

-
-
-            
-              {typeInfo.definition}
-            
-          
-
-
+// Check if a member is new or enhanced compared to parent types +function isNewOrEnhancedMember( + member: TypeMember, + typeInfo: TypeInfo, + parentMembers: Map +): boolean { + const parentMember = parentMembers.get(member.name); - {/* Mixins */} - {typeInfo.mixins && typeInfo.mixins.length > 0 && ( -
-

Extends

-
- {typeInfo.mixins.map((mixin, index) => ( -
- {mixin} -
+ if (!parentMember) { + // Member doesn't exist in parent - it's new + return true; + } + + // Check if this member has signatures not in the parent + const memberSigCount = member.signatures?.length ?? 0; + const parentSigCount = parentMember.signatures?.length ?? 0; + + if (memberSigCount > 0 && parentSigCount > 0) { + // Compare signatures - normalize Server* types + const memberSigs = new Set(member.signatures?.map(s => s.signature) || []); + const parentSigs = new Set(parentMember.signatures?.map(s => s.signature) || []); + + const normalizeServerTypes = (sig: string) => { + return sig + .replace(/Server(ContactChannel|User|Team|Permission|ApiKey|Item|Project|Email)/g, '$1') + .trim(); + }; + + const normalizedMemberSigs = new Set(Array.from(memberSigs).map(normalizeServerTypes)); + const normalizedParentSigs = new Set(Array.from(parentSigs).map(normalizeServerTypes)); + + // Check if there are any new signatures not in parent + const hasNewSignatures = Array.from(normalizedMemberSigs).some(sig => !normalizedParentSigs.has(sig)); + + if (!hasNewSignatures) { + // All member signatures exist in parent - not new + return false; + } + } + + // For methods, compare the most specific signatures + if (member.kind === 'method' && parentMember.kind === 'method') { + const memberSig = member.signatures?.[member.signatures.length - 1]; + const parentSig = parentMember.signatures?.[parentMember.signatures.length - 1]; + + if (memberSig && parentSig) { + // Compare parameters - if they're different, it's enhanced + if (memberSig.signature !== parentSig.signature) { + const memberParams = memberSig.parameters.map(p => `${p.name}:${p.type}`).join(','); + const parentParams = parentSig.parameters.map(p => `${p.name}:${p.type}`).join(','); + + if (memberParams !== parentParams) { + return true; // Different parameters = enhanced + } + } + + // If parameters are identical, check if return type is meaningfully different + // Ignore Server* vs non-Server* variations (e.g., ServerContactChannel vs ContactChannel) + const normalizeServerTypes = (type: string) => { + return type + .replace(/Server(ContactChannel|User|Team|Permission|ApiKey|Item|Project|Email)/g, '$1') + .replace(/\s+/g, ' ') + .trim(); + }; + + const memberReturn = normalizeServerTypes(memberSig.returnType); + const parentReturn = normalizeServerTypes(parentSig.returnType); + + if (memberReturn !== parentReturn) { + return true; // Meaningfully different return type + } + } + } + + // For properties, check if the type is different + if (member.kind === 'property' && parentMember.kind === 'property') { + if (member.type !== parentMember.type) { + return true; + } + } + + // Otherwise, it's the same as parent + return false; +} + +// Extract clean type names from mixin strings (e.g., "Customer" -> "Customer") +function extractTypeNameFromMixin(mixin: string): string | null { + // Skip inline type literals like "{ ... }" + if (mixin.trim().startsWith('{') || mixin.trim().startsWith('|')) { + return null; + } + // Extract base type name (before any generic parameters) + const match = mixin.match(/^([A-Z][a-zA-Z0-9_]*)/); + return match ? match[1] : null; +} + +// Check if a parent type should be shown in inheritance display +// Only show inheritance when there's a clear naming relationship +// e.g., ServerTeam → Team, CurrentServerUser → ServerUser +function shouldShowAsParent(childTypeName: string, parentTypeName: string): boolean { + // Show if the child name contains the parent name (ServerTeam contains Team) + if (childTypeName.includes(parentTypeName)) { + return true; + } + // Show if the parent name contains the child name (rare but possible) + if (parentTypeName.includes(childTypeName)) { + return true; + } + // Don't show "building block" types like Auth, Customer, etc. + // These are implementation details, not types users would look up + return false; +} + +export function TypeDocumentation({ typeInfo, platform = 'react-like', parentTypes = [] }: TypeDocumentationProps) { + const [parentMembers, setParentMembers] = React.useState>(new Map()); + // Track which parent types are actually documented (exist in types.json) + const [documentedParentTypes, setDocumentedParentTypes] = React.useState([]); + const [loading, setLoading] = React.useState(true); + + // Load parent type members and determine which are actually documented + React.useEffect(() => { + async function loadParentTypes() { + try { + const response = await fetch('/sdk-docs/types.json'); + if (!response.ok) { + throw new Error(`Failed to load types.json: ${response.status} ${response.statusText}`); + } + + const typesData = await response.json(); + + // Determine candidate parent types from manual prop or mixins + const candidateParents: string[] = parentTypes.length > 0 + ? parentTypes + : (typeInfo.mixins || []) + .map(extractTypeNameFromMixin) + .filter((name): name is string => name !== null); + + // Only consider parent types that are: + // 1. Actually documented in types.json + // 2. Have a clear naming relationship (ServerTeam → Team) + const documented = candidateParents.filter(name => + typesData[name] !== undefined && + (parentTypes.length > 0 || shouldShowAsParent(typeInfo.name, name)) + ); + setDocumentedParentTypes(documented); + + // Load members from documented parent types + const allParentMembers = new Map(); + for (const parentTypeName of documented) { + const parentType = typesData[parentTypeName]; + if (parentType && parentType.members) { + for (const member of parentType.members) { + if (allParentMembers.has(member.name)) { + const existing = allParentMembers.get(member.name)!; + if (existing.signatures && member.signatures) { + existing.signatures = [...existing.signatures, ...member.signatures]; + } + } else { + allParentMembers.set(member.name, { ...member }); + } + } + } + } + + setParentMembers(allParentMembers); + } finally { + setLoading(false); + } + } + runAsynchronously(loadParentTypes()); + }, [parentTypes, typeInfo.mixins, typeInfo.name]); + + if (loading) { + return
Loading...
; + } + + // Filter members to only show new or enhanced ones + // Priority: 1. Use ownMemberNames if available AND we have documented parent types + // 2. Fall back to signature comparison with documented parent types + // 3. Show all members if no documented parents (nothing to hide) + const filteredMembers = (() => { + // Only filter if we have documented parent types that users can actually reference + if (documentedParentTypes.length === 0) { + // No documented parents - show ALL members (nothing to hide) + return typeInfo.members; + } + + // If we have ownMemberNames, use that directly (most reliable) + if (typeInfo.ownMemberNames && typeInfo.ownMemberNames.length > 0) { + const ownSet = new Set(typeInfo.ownMemberNames); + return typeInfo.members.filter(member => ownSet.has(member.name)); + } + + // Fall back to signature comparison with documented parent types + return typeInfo.members.filter(member => isNewOrEnhancedMember(member, typeInfo, parentMembers)); + })(); + + const inheritedCount = typeInfo.members.length - filteredMembers.length; + + return ( + <> + {/* Inheritance note - only show if there are documented parent types */} + {documentedParentTypes.length > 0 && ( +
+

+ This type extends{' '} + {documentedParentTypes.map((parent, idx) => ( + + {parent} + {idx < documentedParentTypes.length - 1 && (idx === documentedParentTypes.length - 2 ? ' and ' : ', ')} + ))} -

+ {inheritedCount > 0 && ` (${inheritedCount} inherited ${inheritedCount === 1 ? 'member' : 'members'} not shown)`}. + {filteredMembers.length === 0 && ' It does not add any new members.'} +

)} {/* Table of Contents */} - {typeInfo.members.length > 0 && ( -
-

Table of Contents

+ {(filteredMembers.length > 0 || documentedParentTypes.length > 0) && ( +
0 && filteredMembers.length > 0 ? ' (New Members Only)' : ''}`} + code={generateTableOfContents(typeInfo, platform, filteredMembers, documentedParentTypes)} platform={platform} />
)} {/* Members Documentation */} - {typeInfo.members.length > 0 && ( -
-

Members

-
- {typeInfo.members.map(member => + {filteredMembers.map(member => renderMemberDocumentation(typeInfo, member, platform) - )} -
-
)} -
+ ); } -// Component to load and display a specific type from types.json -export function TypeFromJson({ typeName, platform = 'react-like' }: { typeName: string, platform?: string }) { +// Component to load and display a specific type from types.json or mixins.json +export function TypeFromJson({ + typeName, + platform = 'react-like', + parentTypes = [] +}: { + typeName: string, + platform?: string, + parentTypes?: string[], +}) { const [typeInfo, setTypeInfo] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { + const controller = new AbortController(); + async function loadTypeInfo() { - try { - setLoading(true); - setError(null); + setLoading(true); + setError(null); - // Load the types.json file - const response = await fetch('/sdk-docs/types.json'); - if (!response.ok) { - throw new Error(`Failed to load types.json: ${response.statusText}`); + try { + const typesResponse = await fetch('/sdk-docs/types.json', { signal: controller.signal }); + if (typesResponse.ok) { + const typesData: Partial> = await typesResponse.json(); + const found = typesData[typeName]; + if (found) { + if (!controller.signal.aborted) setTypeInfo(found); + return; + } } - const typesData = await response.json(); - const foundType = typesData[typeName]; - - if (!foundType) { - throw new Error(`Type "${typeName}" not found in types.json`); + const mixinsResponse = await fetch('/sdk-docs/mixins.json', { signal: controller.signal }); + if (mixinsResponse.ok) { + const mixinsData: Partial> = await mixinsResponse.json(); + const found = mixinsData[typeName]; + if (found) { + if (!controller.signal.aborted) setTypeInfo(found); + return; + } } - setTypeInfo(foundType); + throw new Error(`Type "${typeName}" not found in types.json or mixins.json`); } catch (err) { + if (controller.signal.aborted) return; setError(err instanceof Error ? err.message : 'Unknown error'); } finally { - setLoading(false); + if (!controller.signal.aborted) { + setLoading(false); + } } } runAsynchronously(loadTypeInfo()); + + return () => controller.abort(); }, [typeName]); if (loading) { @@ -355,5 +1179,5 @@ export function TypeFromJson({ typeName, platform = 'react-like' }: { typeName: ); } - return ; + return ; } diff --git a/docs/src/components/ui/method-layout.tsx b/docs/src/components/ui/method-layout.tsx index e30a18ed99..c264d011d1 100644 --- a/docs/src/components/ui/method-layout.tsx +++ b/docs/src/components/ui/method-layout.tsx @@ -294,7 +294,9 @@ export function CollapsibleTypesSection({ defaultOpen = true, type, property, - signature + signature, + isReactHook = false, + isDeprecated = false }: { children: ReactNode, className?: string, @@ -302,6 +304,8 @@ export function CollapsibleTypesSection({ type: string, property: string, signature?: string, + isReactHook?: boolean, + isDeprecated?: boolean, }) { // Generate anchor ID for types (e.g., currentUser.id -> currentuserid) const typeName = type.replace(/[^a-z0-9]/gi, '').toLowerCase(); @@ -402,6 +406,16 @@ export function CollapsibleTypesSection({
+ {isDeprecated && ( + + Deprecated + + )} + {isReactHook && ( + + React + + )} {type && ( {type} diff --git a/docs/src/mdx-components.tsx b/docs/src/mdx-components.tsx index 5970bc5859..9ee95d77f2 100644 --- a/docs/src/mdx-components.tsx +++ b/docs/src/mdx-components.tsx @@ -18,7 +18,10 @@ import { PlatformCodeblock } from './components/mdx/platform-codeblock'; import { AsideSection, CollapsibleMethodSection, CollapsibleTypesSection, MethodAside, MethodContent, MethodLayout, MethodSection, MethodTitle } from './components/ui/method-layout'; +import { HookDocumentation, HookFromJson } from './components/sdk/hook-documentation'; +import { ObjectDocumentation, ObjectFromJson } from './components/sdk/object-documentation'; import { SDKOverview } from './components/sdk/overview'; +import { TypeDocumentation, TypeFromJson } from './components/sdk/type-documentation'; import { CursorIcon, StackAuthIcon } from './components/icons'; import { Button } from './components/mdx/button'; @@ -86,6 +89,12 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { CollapsibleMethodSection, CollapsibleTypesSection, SDKOverview, + TypeDocumentation, + TypeFromJson, + HookDocumentation, + HookFromJson, + ObjectDocumentation, + ObjectFromJson, AppleSecretGenerator, // Logo Icons StackAuthIcon, diff --git a/package.json b/package.json index 9f93e85fe9..fd50ae9b9a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:backend": "pnpm pre && turbo run build --filter=@stackframe/backend...", "build:dashboard": "pnpm pre && turbo run build --filter=@stackframe/dashboard...", "build:demo": "pnpm pre && turbo run build --filter=demo-app...", - "build:docs": "pnpm run build:packages && pnpm run codegen && pnpm run build:backend && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs && turbo run build --filter=@stackframe/stack-docs", + "build:docs": "pnpm run build:packages && pnpm run codegen && pnpm run build:backend && pnpm run docs:generate-sdk-type-reference && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs && turbo run build --filter=@stackframe/stack-docs", "build:packages": "pnpm pre && turbo run build --filter=./packages/*", "restart-dev-in-background": "pnpm pre && pnpm run kill-dev:named && (pnpm run dev:named > dev-server.log.untracked.txt 2>&1 &) && echo 'Starting dev server in background... (Logs are in dev-server.log.untracked.txt)' && pnpx wait-on http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02 -t 120000 && echo 'Dev server running.'", "restart-dev-environment": "pnpm pre && pnpm run build:packages && pnpm run codegen && pnpm run restart-deps && pnpm run restart-dev-in-background", @@ -68,7 +68,10 @@ "generate-keys": "pnpm pre && turbo run generate-keys", "generate-sdks": "pnpx --package=tsx tsx ./scripts/generate-sdks.ts", "generate-sdks:watch": "chokidar --silent -c 'pnpm run generate-sdks' './packages/template' --ignore './packages/template/package.json' --ignore '**/node_modules/**' --ignore '**/dist/**' --ignore '**/.turbo/**' --throttle 2000", - "generate-openapi-docs:watch": "chokidar --silent -c 'pnpm run --filter=@stackframe/stack-docs generate-openapi-docs' './docs/public/openapi/{admin,client,server,webhooks}.json' --throttle 2000" + "generate-docs": "pnpm run build:packages-only && turbo run generate-openapi-fumadocs && pnpm run --filter=@stackframe/stack-docs generate-docs && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs", + "generate-docs:watch": "chokidar --silent -c 'pnpm run generate-docs' './docs/templates' --throttle 2000", + "generate-openapi-docs:watch": "chokidar --silent -c 'pnpm run --filter=@stackframe/stack-docs generate-openapi-docs' './docs/public/openapi/{admin,client,server,webhooks}.json' --throttle 2000", + "docs:generate-sdk-type-reference": "cd packages/template && node scripts/generate-sdk-type-reference.cjs && mkdir -p ../../docs/public/sdk-docs && cp dist/sdk-docs/*.json ../../docs/public/sdk-docs/" }, "devDependencies": { "@changesets/cli": "^2.27.9", diff --git a/packages/template/scripts/generate-sdk-type-reference.cjs b/packages/template/scripts/generate-sdk-type-reference.cjs new file mode 100644 index 0000000000..3905c3fc16 --- /dev/null +++ b/packages/template/scripts/generate-sdk-type-reference.cjs @@ -0,0 +1,891 @@ +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); + +const ROOT_DIR = path.resolve(__dirname, '..'); +const SRC_DIR = path.join(ROOT_DIR, 'src'); +const OUTPUT_DIR = path.join(ROOT_DIR, 'dist', 'sdk-docs'); +const TSCONFIG_PATH = path.join(ROOT_DIR, 'tsconfig.json'); + +// ============================================================ +// CONFIGURATION: Tag-based filtering +// ============================================================ +// USE_TAG_FILTER: Master switch for tag-based filtering +// - false: Document ALL exported items regardless of JSDoc tags (default behavior) +// - true: Apply tag-based filtering according to TAG_FILTER_MODE +const USE_TAG_FILTER = false; + +// TAG_FILTER_MODE: Filtering strategy when USE_TAG_FILTER is true +// This setting is IGNORED when USE_TAG_FILTER is false +// +// - 'opt-in' (Whitelist): Only document items explicitly marked with @stackdoc +// Use this when you want strict control over what's documented +// Example: export function myFunc() { } // Not documented +// /** @stackdoc */ export function myFunc() { } // Documented +// +// - 'opt-out' (Blacklist): Document all items EXCEPT those marked with @internal +// Use this when most things should be documented but you want to hide internals +// Example: export function myFunc() { } // Documented +// /** @internal */ export function myFunc() { } // Not documented +const TAG_FILTER_MODE = 'opt-in'; + +// MIXIN_TYPES: Types that are building blocks/mixins composed into other types +// These will be output to mixins.json instead of types.json +const MIXIN_TYPES = new Set([ + 'Auth', + 'Customer', + 'Connection', + 'AuthLike', + 'BaseUser', + 'UserExtra', +]); + +// Auto-generate descriptions for AsyncStoreProperty-style methods +// These are mapped type methods that don't have JSDoc attached +function generateAsyncStorePropertyDescription(name) { + // Helper to format resource name (e.g., "ApiKeys" -> "API keys") + function formatResource(str) { + return str + .replace(/([A-Z])/g, ' $1') + .trim() + .toLowerCase() + .replace(/\bapi\b/g, 'API'); + } + + // list{Name}s -> "Returns all {name}." + const listMatch = name.match(/^list([A-Z][a-zA-Z]*)$/); + if (listMatch) { + const resource = formatResource(listMatch[1]); + return `Returns all ${resource}.`; + } + + // get{Name} -> "Returns the {name}, or null if not found." + const getMatch = name.match(/^get([A-Z][a-zA-Z]*)$/); + if (getMatch) { + const resource = formatResource(getMatch[1]); + return `Returns the ${resource}, or null if not found.`; + } + + // use{Name}s (plural) -> "React hook that returns all {name}." + const useListMatch = name.match(/^use([A-Z][a-zA-Z]*s)$/); + if (useListMatch && !name.endsWith('ss')) { + const resource = formatResource(useListMatch[1]); + return `React hook that returns all ${resource}.`; + } + + // use{Name} -> "React hook that returns the {name}." + const useMatch = name.match(/^use([A-Z][a-zA-Z]*)$/); + if (useMatch) { + const resource = formatResource(useMatch[1]); + return `React hook that returns the ${resource}.`; + } + + return null; +} +// ============================================================ + +const TYPE_FORMAT_FLAGS = + ts.TypeFormatFlags.NoTruncation | + ts.TypeFormatFlags.UseFullyQualifiedType | + ts.TypeFormatFlags.WriteArrowStyleSignature; + +// Flags for expanding type aliases inline in signatures +const TYPE_FORMAT_FLAGS_INLINE = + ts.TypeFormatFlags.NoTruncation | + ts.TypeFormatFlags.InTypeAlias | + ts.TypeFormatFlags.WriteArrayAsGenericType | + ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | + ts.TypeFormatFlags.WriteArrowStyleSignature; + +// Flags for keeping type references without expansion +const TYPE_FORMAT_FLAGS_NO_EXPAND = + ts.TypeFormatFlags.NoTruncation | + ts.TypeFormatFlags.WriteArrowStyleSignature; + +// Determine if a type should be expanded inline or kept as a reference +function shouldExpandType(type, checker) { + const symbol = type.getSymbol(); + if (!symbol) return false; + + const name = symbol.getName(); + + // Expand types that are clearly "configuration" or "options" objects + if (name.endsWith('Options') || name.endsWith('Config') || name.endsWith('Props') || name.endsWith('Data')) { + return true; + } + + // Don't expand types that are likely documented entities + // These patterns indicate domain objects that users should look up separately + const entityPatterns = [ + 'User', 'Team', 'Project', 'Permission', 'ApiKey', 'Connection', + 'Channel', 'Session', 'Auth', 'Customer', 'Item', 'Email' + ]; + + for (const pattern of entityPatterns) { + if (name.includes(pattern) && !name.endsWith('Options')) { + return false; + } + } + + // Expand anonymous/inline types (no meaningful name) + if (name === '__type' || name === '__object') { + return true; + } + + // Default: don't expand (keep as reference) + return false; +} + +// Function to expand type aliases and show inline object types +function expandType(type, checker, node) { + // Check if it's an empty tuple type (represented as []) + if (checker.isTupleType(type)) { + const typeArgs = type.typeArguments || []; + if (typeArgs.length === 0) { + // Empty tuple - this represents no parameters + return '[]'; + } + } + + // Check if we should expand this type based on its name/symbol + const symbol = type.getSymbol(); + if (symbol) { + const shouldExpand = shouldExpandType(type, checker); + + if (!shouldExpand) { + // Don't expand - keep as type reference + return checker.typeToString(type, node, TYPE_FORMAT_FLAGS_NO_EXPAND); + } + + // For type aliases that should be expanded, expand them + const properties = checker.getPropertiesOfType(type); + if (properties.length > 0 && properties.length < 20) { + const props = properties.map(prop => { + const propType = checker.getTypeOfSymbolAtLocation(prop, node); + const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0 ? '?' : ''; + // Use NO_EXPAND for property types to avoid recursive explosion + return `${prop.getName()}${optional}: ${checker.typeToString(propType, node, TYPE_FORMAT_FLAGS_NO_EXPAND)}`; + }).join('; '); + return `{ ${props}; }`; + } + } + + // Handle anonymous intersection types (no symbol) + if (type.flags & ts.TypeFlags.Intersection) { + const types = type.types || []; + + // Check if any part contains Options/Config types + const hasOptionsType = types.some(t => { + const sym = t.getSymbol(); + if (!sym) return true; // Inline types + const name = sym.getName(); + return name.endsWith('Options') || name.endsWith('Config') || name.endsWith('Data'); + }); + + if (hasOptionsType) { + return types.map(t => expandType(t, checker, node)).join(' & '); + } else { + // Entity intersection types like Team - keep as reference + return checker.typeToString(type, node, TYPE_FORMAT_FLAGS_NO_EXPAND); + } + } + + // For inline object types (no symbol) + if (type.flags & ts.TypeFlags.Object) { + const properties = checker.getPropertiesOfType(type); + if (properties.length > 0 && properties.length < 10) { + const props = properties.map(prop => { + const propType = checker.getTypeOfSymbolAtLocation(prop, node); + const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0 ? '?' : ''; + return `${prop.getName()}${optional}: ${checker.typeToString(propType, node, TYPE_FORMAT_FLAGS_INLINE)}`; + }).join('; '); + return `{ ${props}; }`; + } + } + + // Default to standard type string (keeps type aliases) + return checker.typeToString(type, node, TYPE_FORMAT_FLAGS_NO_EXPAND); +} + +// Extract property descriptions AND types from a type +function extractPropertyDescriptions(type, checker, node) { + const propertyInfo = {}; + + // Skip ALL tuple types (including named tuples like [scope: Team, id: string]) + if (checker.isTupleType(type)) { + return propertyInfo; + } + + // Check if we should extract descriptions for this type + const symbol = type.getSymbol(); + if (symbol && !shouldExpandType(type, checker)) { + // Don't extract - this is an entity type that should stay as a reference + return propertyInfo; + } + + // Don't extract from intersection types unless they contain Options types + if (type.flags & ts.TypeFlags.Intersection) { + const types = type.types || []; + + // Check if any part is an Options type + const hasOptionsType = types.some(t => { + const sym = t.getSymbol(); + if (!sym) return false; + const name = sym.getName(); + return name.endsWith('Options') || name.endsWith('Config') || name.endsWith('Data'); + }); + + if (hasOptionsType) { + // Extract from Options types only + for (const t of types) { + Object.assign(propertyInfo, extractPropertyDescriptions(t, checker, node)); + } + } + + return propertyInfo; + } + + if (type.flags & ts.TypeFlags.Object) { + // We already checked shouldExpandType above + const properties = checker.getPropertiesOfType(type); + for (const prop of properties) { + const description = ts.displayPartsToString(prop.getDocumentationComment(checker)); + const propType = checker.getTypeOfSymbolAtLocation(prop, node); + const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0; + + propertyInfo[prop.getName()] = { + type: checker.typeToString(propType, node, TYPE_FORMAT_FLAGS_NO_EXPAND), + optional, + description: description || undefined + }; + } + } + + return propertyInfo; +} + +function readTsConfig(configPath) { + const configFile = ts.readConfigFile(configPath, ts.sys.readFile); + if (configFile.error) { + throw new Error(ts.flattenDiagnosticMessageText(configFile.error.messageText, '\n')); + } + return ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath)); +} + +/** + * Check if a node has the @stackdoc JSDoc tag + * This allows developers to explicitly mark items for SDK documentation + */ +function hasStackDocTag(node) { + if (!node) return false; + const jsDocs = ts.getJSDocTags(node); + return jsDocs.some(tag => tag.tagName.text === 'stackdoc'); +} + +/** + * Check if a node has the @internal JSDoc tag + * Items marked as @internal will be excluded from docs + */ +function hasInternalTag(node) { + if (!node) return false; + const jsDocs = ts.getJSDocTags(node); + return jsDocs.some(tag => tag.tagName.text === 'internal'); +} + +function includeDeclaration(filePath) { + const relative = path.relative(SRC_DIR, filePath); + if (relative.startsWith('lib/stack-app')) return true; + if (relative.startsWith('lib/hooks')) return true; + return false; +} + +function getCategory(name, declaration) { + if (name.startsWith('use') && name.length > 3 && name[3] === name[3].toUpperCase()) { + return 'hooks'; + } + const relative = path.relative(SRC_DIR, declaration.getSourceFile().fileName); + if (relative.startsWith('lib/stack-app/apps')) { + return 'objects'; + } + if (ts.isTypeAliasDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) { + // Check if this is a mixin type + if (MIXIN_TYPES.has(name)) { + return 'mixins'; + } + return 'types'; + } + return 'objects'; +} + +function getKind(declaration) { + switch (declaration.kind) { + case ts.SyntaxKind.InterfaceDeclaration: + return 'interface'; + case ts.SyntaxKind.TypeAliasDeclaration: + return 'type'; + case ts.SyntaxKind.FunctionDeclaration: + return 'function'; + case ts.SyntaxKind.VariableDeclaration: + return 'variable'; + case ts.SyntaxKind.ClassDeclaration: + return 'class'; + case ts.SyntaxKind.EnumDeclaration: + return 'enum'; + default: + return ts.SyntaxKind[declaration.kind] || 'unknown'; + } +} + +function normaliseTags(tags) { + const results = tags.map(tag => { + let text; + if (Array.isArray(tag.text)) { + text = tag.text.map(part => part.text).join(''); + } else { + text = tag.text; + } + return { + name: tag.name, + text: text || undefined, + }; + }).filter(tag => tag.text !== undefined); + return results.length ? results : undefined; +} + +function extractPlatformTags(node) { + if (!node) return undefined; + const sourceFile = node.getSourceFile(); + const text = sourceFile.getFullText(); + const ranges = [ + ...(ts.getLeadingCommentRanges(text, node.getFullStart()) || []), + ...(ts.getTrailingCommentRanges(text, node.getEnd()) || []), + ]; + + const platforms = new Set(); + for (const range of ranges) { + const comment = text.slice(range.pos, range.end); + const matches = comment.matchAll(/PLATFORM\s+([a-zA-Z0-9-]+)/g); + for (const match of matches) { + platforms.add(match[1]); + } + } + + return platforms.size ? Array.from(platforms) : undefined; +} + +function printers() { + const printer = ts.createPrinter({ removeComments: false }); + return { + print(node) { + return printer.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile()); + } + }; +} + +const { print } = printers(); + +function selectDeclaration(declarations, preferredSourcePath) { + if (!declarations || !declarations.length) return undefined; + if (!preferredSourcePath) return declarations[0]; + const preferred = declarations.find(decl => path.relative(ROOT_DIR, decl.getSourceFile().fileName) === preferredSourcePath); + return preferred || declarations[0]; +} + +function createBaseEntry(symbol, declaration, checker) { + const sourceFile = declaration.getSourceFile(); + const position = sourceFile.getLineAndCharacterOfPosition(declaration.getStart()); + const description = ts.displayPartsToString(symbol.getDocumentationComment(checker)); + const tags = normaliseTags(symbol.getJsDocTags()); + const entry = { + name: symbol.getName(), + kind: getKind(declaration), + sourcePath: path.relative(ROOT_DIR, sourceFile.fileName), + line: position.line + 1, + }; + if (description) entry.description = description; + if (tags) entry.tags = tags; + return entry; +} + +function gatherMixinsFromTypeNode(node, mixins) { + if (!node) return; + if (ts.isIntersectionTypeNode(node)) { + node.types.forEach(typeNode => gatherMixinsFromTypeNode(typeNode, mixins)); + return; + } + if (ts.isTypeLiteralNode(node)) { + return; + } + if (ts.isParenthesizedTypeNode(node)) { + gatherMixinsFromTypeNode(node.type, mixins); + return; + } + mixins.push(node.getText()); +} + +/** + * Extract member names that are explicitly defined in the type's own body + * (not inherited from mixins/parent types). + * + * For a type like: + * type ServerTeam = { createdAt: Date; addUser(): void; } & Team; + * + * This returns ['createdAt', 'addUser'] - the members defined inline, + * NOT the members inherited from Team. + */ +function gatherOwnMemberNames(node, memberNames) { + if (!node) return; + + if (ts.isIntersectionTypeNode(node)) { + // For intersection types, only gather from type literals (inline definitions) + node.types.forEach(typeNode => gatherOwnMemberNames(typeNode, memberNames)); + return; + } + + if (ts.isTypeLiteralNode(node)) { + // This is an inline object type - extract its member names + node.members.forEach(member => { + if (ts.isPropertySignature(member) || ts.isMethodSignature(member)) { + const name = member.name; + if (name && ts.isIdentifier(name)) { + memberNames.push(name.text); + } + } + }); + return; + } + + if (ts.isParenthesizedTypeNode(node)) { + gatherOwnMemberNames(node.type, memberNames); + return; + } + + // For type references (like `Team`, `BaseUser`), we don't extract members + // because those are inherited, not "own" members +} + +function buildMemberEntry(symbol, parentDeclaration, checker) { + const declarations = symbol.getDeclarations() || []; + const parentPath = path.relative(ROOT_DIR, parentDeclaration.getSourceFile().fileName); + const declaration = selectDeclaration(declarations, parentPath); + const fallbackNode = declaration || parentDeclaration; + const sourceFile = declaration && declaration.getSourceFile(); + const location = declaration && sourceFile.getLineAndCharacterOfPosition(declaration.getStart()); + + // Try to get description from symbol first, then fallback to AST JSDoc nodes + // (needed for properties in type literals within intersection types) + let description = ts.displayPartsToString(symbol.getDocumentationComment(checker)); + let tags = normaliseTags(symbol.getJsDocTags()); + + // When a property exists in multiple types of an intersection, TypeScript concatenates + // their JSDoc comments. Prefer the description from the selected declaration only. + if (description && declaration) { + const jsDocNodes = ts.getJSDocCommentsAndTags(declaration); + for (const jsDoc of jsDocNodes) { + if (ts.isJSDoc(jsDoc) && jsDoc.comment) { + let declDescription; + if (typeof jsDoc.comment === 'string') { + declDescription = jsDoc.comment; + } else if (Array.isArray(jsDoc.comment)) { + declDescription = jsDoc.comment.map(part => part.text || '').join(''); + } + // If we have a declaration-specific description, use it instead of the merged one + if (declDescription && description !== declDescription && description.includes(declDescription)) { + description = declDescription; + } + break; + } + } + } + + if (!description && declaration) { + const jsDocNodes = ts.getJSDocCommentsAndTags(declaration); + for (const jsDoc of jsDocNodes) { + if (ts.isJSDoc(jsDoc) && jsDoc.comment) { + // Extract comment text + if (typeof jsDoc.comment === 'string') { + description = jsDoc.comment; + } else if (Array.isArray(jsDoc.comment)) { + description = jsDoc.comment.map(part => part.text || '').join(''); + } + // Also extract tags from this JSDoc node + if (jsDoc.tags && jsDoc.tags.length > 0 && (!tags || tags.length === 0)) { + tags = normaliseTags(jsDoc.tags.map(tag => ({ + name: tag.tagName.text, + text: tag.comment ? (typeof tag.comment === 'string' ? tag.comment : tag.comment.map(p => p.text).join('')) : '' + }))); + } + break; + } + } + } + const symbolType = checker.getTypeOfSymbolAtLocation(symbol, fallbackNode); + const callSignatures = symbolType.getCallSignatures(); + + const name = symbol.getName(); + + // Skip internal members (prefix with underscore) + if (name.startsWith('_')) { + return null; + } + + // If no description, try to extract from @deprecated tag text + // (some JSDoc puts the description after @deprecated "Use X instead.") + if (!description && tags && tags.length > 0) { + const deprecatedTagIndex = tags.findIndex(t => t.name === 'deprecated'); + if (deprecatedTagIndex !== -1) { + const deprecatedTag = tags[deprecatedTagIndex]; + if (deprecatedTag.text) { + const text = deprecatedTag.text; + // Match "Use X instead." at the start, followed by description + const useMatch = text.match(/^(Use [^.]+\.)\s*(.+)/s); + if (useMatch && useMatch[2]) { + description = useMatch[2].trim(); + // Update the deprecated tag to only contain "Use X instead." + tags[deprecatedTagIndex] = { ...deprecatedTag, text: useMatch[1] }; + } else if (!text.toLowerCase().startsWith('use ')) { + // If it doesn't start with "Use", the whole text might be the description + description = text; + // Clear the deprecated tag text since we used it as description + tags[deprecatedTagIndex] = { ...deprecatedTag, text: '' }; + } + } + } + } + + // Auto-generate descriptions for AsyncStoreProperty-style methods (last resort) + if (!description) { + description = generateAsyncStorePropertyDescription(name); + } + + const entry = { + name, + optional: (symbol.flags & ts.SymbolFlags.Optional) !== 0, + }; + + if (description) entry.description = description; + if (tags) entry.tags = tags; + if (declaration) { + entry.sourcePath = path.relative(ROOT_DIR, declaration.getSourceFile().fileName); + entry.line = location.line + 1; + const platforms = extractPlatformTags(declaration); + if (platforms) entry.platforms = platforms; + } + + if (callSignatures.length > 0) { + entry.kind = 'method'; + const allSignatures = callSignatures.map(signature => { + const parameters = signature.getParameters() + .map(paramSymbol => { + const paramDecl = paramSymbol.valueDeclaration || (paramSymbol.declarations && paramSymbol.declarations[0]); + const paramType = checker.getTypeOfSymbolAtLocation(paramSymbol, paramDecl || fallbackNode); + const expandedType = expandType(paramType, checker, paramDecl || fallbackNode); + const optional = (paramSymbol.flags & ts.SymbolFlags.Optional) !== 0 || + (!!paramDecl && ts.isParameter(paramDecl) && !!paramDecl.questionToken); + + // Skip empty tuple parameters (these represent no actual parameters) + if (expandedType === '[]') { + return null; + } + + // Extract property descriptions for object types + const propertyDescriptions = extractPropertyDescriptions(paramType, checker, paramDecl || fallbackNode); + + const param = { + name: paramSymbol.getName(), + type: expandedType, + optional, + }; + + // Only add propertyDescriptions if there are any + if (Object.keys(propertyDescriptions).length > 0) { + param.propertyDescriptions = propertyDescriptions; + } + + return param; + }) + .filter(p => p !== null); + + // Build signature string with expanded types + const paramStrings = parameters.map(p => `${p.name}${p.optional ? '?' : ''}: ${p.type}`); + + // Try to get return type from source annotation if available + let returnType; + if (declaration && ts.isFunctionDeclaration(declaration) && declaration.type) { + // Use the source annotation directly + returnType = declaration.type.getText(); + } else if (declaration && ts.isMethodSignature(declaration) && declaration.type) { + returnType = declaration.type.getText(); + } else if (declaration && ts.isPropertySignature(declaration) && declaration.type) { + // For property signatures, try to extract return type + const typeNode = declaration.type; + if (ts.isFunctionTypeNode(typeNode) && typeNode.type) { + returnType = typeNode.type.getText(); + } else { + // Don't expand return types - keep type aliases + returnType = checker.typeToString( + signature.getReturnType(), + fallbackNode, + ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.WriteArrowStyleSignature + ); + } + } else { + // Fallback to checker - don't expand type aliases in return types + returnType = checker.typeToString( + signature.getReturnType(), + fallbackNode, + ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.WriteArrowStyleSignature + ); + } + + const signatureString = `(${paramStrings.join(', ')}) => ${returnType}`; + + return { + signature: signatureString, + parameters, + returnType, + }; + }); + + // Filter out tuple-based signatures (e.g., args: [param1, param2]) + // These come from AsyncStoreProperty and are less user-friendly + const nonTupleSignatures = allSignatures.filter(sig => { + // Check if any parameter is a tuple type + const hasTupleParam = sig.parameters.some(p => p.type.match(/^\[.+\]$/)); + return !hasTupleParam; + }); + + // If there are only tuple signatures, try to unpack them + let signaturesToUse = nonTupleSignatures; + + if (nonTupleSignatures.length === 0 && allSignatures.length > 0) { + // Try to unpack tuple signatures like args: [param1: Type1, param2?: Type2] + const unpackedSignatures = allSignatures.map(sig => { + if (sig.parameters.length === 1 && sig.parameters[0].type.match(/^\[.+\]$/)) { + // This is a tuple parameter - try to unpack it + const tupleParam = sig.parameters[0]; + // Parse the tuple: [param1: Type1, param2?: Type2] -> extract individual params + const tupleContent = tupleParam.type.slice(1, -1); // Remove [ ] + + // Unpack the tuple signature + // From: (args: [options?: Type]) => Return + // To: (options?: Type) => Return + const unpackedSignature = sig.signature.replace(/\(args: \[(.+?)\]\)/, '($1)'); + + return { + ...sig, + signature: unpackedSignature, + parameters: [] // Simplified for now - could parse tuple content + }; + } + return sig; + }); + + signaturesToUse = unpackedSignatures; + } + + // Deduplicate signatures by normalizing Server* types + const normalizeServerTypes = (sig) => { + return sig.replace(/Server(ContactChannel|User|Team|Permission|ApiKey|Item|Project|Email)/g, '$1'); + }; + + const seenSignatures = new Set(); + const uniqueSignatures = []; + + // Process signatures in reverse (keep most specific ones) + for (let i = signaturesToUse.length - 1; i >= 0; i--) { + const sig = signaturesToUse[i]; + const normalized = normalizeServerTypes(sig.signature); + + if (!seenSignatures.has(normalized)) { + seenSignatures.add(normalized); + uniqueSignatures.unshift(sig); // Add to beginning to maintain order + } + } + + entry.signatures = uniqueSignatures; + } else { + entry.kind = 'property'; + entry.type = checker.typeToString(symbolType, fallbackNode, TYPE_FORMAT_FLAGS); + } + + return entry; +} + +function buildTypeEntry(symbol, declaration, checker) { + const base = createBaseEntry(symbol, declaration, checker); + const declaredType = checker.getDeclaredTypeOfSymbol(symbol); + const parentPath = base.sourcePath; + const members = checker + .getPropertiesOfType(declaredType) + .map(propSymbol => buildMemberEntry(propSymbol, declaration, checker)); + + const sortedMembers = members + .filter(member => member !== null) // Filter out internal members + .map(member => ({ + member, + priority: member.sourcePath + ? (member.sourcePath === parentPath ? 0 : 1) + : 2, + order: member.line ?? Number.MAX_SAFE_INTEGER, + })) + .sort((a, b) => + a.priority - b.priority || + a.order - b.order || + a.member.name.localeCompare(b.member.name), + ) + .map(item => item.member); + + const entry = { + ...base, + category: 'types', + definition: print(declaration).trim(), + members: sortedMembers, + }; + + if (ts.isInterfaceDeclaration(declaration) && declaration.heritageClauses) { + const extendsList = declaration.heritageClauses + .filter(clause => clause.token === ts.SyntaxKind.ExtendsKeyword) + .flatMap(clause => clause.types.map(type => type.getText())); + if (extendsList.length) { + entry.extends = extendsList; + } + } + + if (ts.isTypeAliasDeclaration(declaration)) { + const mixins = []; + gatherMixinsFromTypeNode(declaration.type, mixins); + if (mixins.length) { + entry.mixins = mixins; + } + + // Extract member names that are explicitly defined in this type's body + // (not inherited from mixins) + const ownMemberNames = []; + gatherOwnMemberNames(declaration.type, ownMemberNames); + if (ownMemberNames.length) { + entry.ownMemberNames = ownMemberNames; + } + } + + return entry; +} + +function buildGeneralEntry(symbol, declaration, checker, category) { + const base = createBaseEntry(symbol, declaration, checker); + const type = checker.getTypeOfSymbolAtLocation(symbol, declaration); + const callSignatures = type.getCallSignatures(); + const constructSignatures = type.getConstructSignatures(); + + const entry = { + ...base, + category, + type: checker.typeToString(type, declaration, TYPE_FORMAT_FLAGS), + declaration: print(declaration).trim(), + }; + + const signatures = [...callSignatures, ...constructSignatures].map(sig => + checker.signatureToString(sig, declaration, TYPE_FORMAT_FLAGS), + ); + if (signatures.length) { + entry.signatures = signatures; + } + + return entry; +} + +function mapToRecord(map) { + const record = {}; + const entries = Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name)); + for (const entry of entries) { + record[entry.name] = entry; + } + return record; +} + +function collectDocs() { + const parsed = readTsConfig(TSCONFIG_PATH); + const program = ts.createProgram({ + rootNames: parsed.fileNames, + options: parsed.options, + }); + const checker = program.getTypeChecker(); + const processed = new Set(); + const results = { + objects: new Map(), + types: new Map(), + hooks: new Map(), + mixins: new Map(), + }; + + const entryFile = program.getSourceFile(path.join(SRC_DIR, 'index.ts')); + if (!entryFile) { + throw new Error(`Could not find src/index.ts. Current directory: ${process.cwd()}, SRC_DIR: ${SRC_DIR}`); + } + const entrySymbol = checker.getSymbolAtLocation(entryFile); + if (!entrySymbol) { + throw new Error(`Could not resolve exports for src/index.ts. The file may be empty or have syntax errors.`); + } + + const exportedSymbols = checker.getExportsOfModule(entrySymbol); + for (const exported of exportedSymbols) { + let symbol = exported; + if (symbol.flags & ts.SymbolFlags.Alias) { + symbol = checker.getAliasedSymbol(symbol); + } + const declarations = symbol.getDeclarations() || []; + for (const declaration of declarations) { + const fileName = declaration.getSourceFile().fileName; + if (!fileName.startsWith(SRC_DIR)) continue; + if (!includeDeclaration(fileName)) continue; + + // Tag-based filtering + if (USE_TAG_FILTER) { + if (TAG_FILTER_MODE === 'opt-in') { + // Opt-in mode: only include items with @stackdoc tag + if (!hasStackDocTag(declaration)) { + continue; + } + } else if (TAG_FILTER_MODE === 'opt-out') { + // Opt-out mode: exclude items with @internal tag + if (hasInternalTag(declaration)) { + continue; + } + } + } + + const key = `${path.relative(SRC_DIR, fileName)}::${symbol.getName()}::${declaration.pos}`; + if (processed.has(key)) continue; + processed.add(key); + + const category = getCategory(symbol.getName(), declaration); + if (category === 'types' || category === 'mixins') { + const entry = buildTypeEntry(symbol, declaration, checker); + results[category].set(entry.name, entry); + } else { + const entry = buildGeneralEntry(symbol, declaration, checker, category); + results[category].set(entry.name, entry); + } + } + } + + return { + objects: mapToRecord(results.objects), + types: mapToRecord(results.types), + hooks: mapToRecord(results.hooks), + mixins: mapToRecord(results.mixins), + }; +} + +function main() { + const docs = collectDocs(); + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + for (const category of ['objects', 'types', 'hooks', 'mixins']) { + const filePath = path.join(OUTPUT_DIR, `${category}.json`); + fs.writeFileSync(filePath, JSON.stringify(docs[category], null, 2)); + console.log(` Generated ${category}.json`); + } + console.log(`SDK type reference generated at ${OUTPUT_DIR}`); +} + +main(); diff --git a/packages/template/src/lib/hooks.tsx b/packages/template/src/lib/hooks.tsx index 4a2cc568c1..7a45875c2f 100644 --- a/packages/template/src/lib/hooks.tsx +++ b/packages/template/src/lib/hooks.tsx @@ -8,7 +8,7 @@ type GetUserOptions = AppGetUserOptions & { /** * Returns the current user object. Equivalent to `useStackApp().useUser()`. - * + * @stackdoc * @returns the current user */ export function useUser(options: GetUserOptions & { or: 'redirect' | 'throw', projectIdMustMatch: "internal" }): CurrentInternalUser; @@ -29,7 +29,7 @@ export function useUser(options: GetUserOptions = {}): CurrentUser | CurrentInte /** * Returns the current Stack app associated with the StackProvider. - * + * @stackdoc * @returns the current Stack app */ export function useStackApp(options: { projectIdMustMatch?: ProjectId } = {}): StackClientApp { diff --git a/packages/template/src/lib/stack-app/api-keys/index.ts b/packages/template/src/lib/stack-app/api-keys/index.ts index 338bb3e843..7597dcb3eb 100644 --- a/packages/template/src/lib/stack-app/api-keys/index.ts +++ b/packages/template/src/lib/stack-app/api-keys/index.ts @@ -5,17 +5,61 @@ import type * as yup from "yup"; export type ApiKeyType = "user" | "team"; +/** + * Represents an API key for programmatic authentication. + * Can be associated with either a user or a team. + */ export type ApiKey = & { + /** + * The unique identifier for this API key. + */ id: string, + + /** + * A human-readable description of the API key's purpose. + */ description: string, + + /** + * The date and time when this API key will expire. If not set, the key does not expire. + */ expiresAt?: Date, + + /** + * The date when the key was manually revoked, or null if not revoked. + */ manuallyRevokedAt?: Date | null, + + /** + * The date and time when this API key was created. + */ createdAt: Date, + + /** + * The API key value. On first view (after creation), this is the full key string. + * In subsequent views (from list methods), this is an object with only the last four characters. + */ value: IfAndOnlyIf, + + /** + * Updates the API key properties. + */ update(options: ApiKeyUpdateOptions): Promise, + + /** + * Revokes the API key, making it permanently invalid. + */ revoke: () => Promise, + + /** + * Returns whether the API key is currently valid (not expired and not revoked). + */ isValid: () => boolean, + + /** + * Returns the reason why the key is invalid, or null if it's valid. + */ whyInvalid: () => "manually-revoked" | "expired" | null, } & ( @@ -23,10 +67,24 @@ export type ApiKey>; + +/** + * User API key with masked value (returned by listApiKeys/useApiKeys). + */ export type UserApiKey = PrettifyType>; +/** + * Team API key with full value visible (returned by createApiKey). + */ export type TeamApiKeyFirstView = PrettifyType>; + +/** + * Team API key with masked value (returned by listApiKeys/useApiKeys). + */ export type TeamApiKey = PrettifyType>; export type ApiKeyCreationOptions = diff --git a/packages/template/src/lib/stack-app/common.ts b/packages/template/src/lib/stack-app/common.ts index 331db26ef6..1a325cd8bb 100644 --- a/packages/template/src/lib/stack-app/common.ts +++ b/packages/template/src/lib/stack-app/common.ts @@ -126,6 +126,9 @@ export type OAuthScopesOnSignIn = { * Used for apps that have token storage capabilities. */ export type AuthLike = { + /** + * Signs out the current user and optionally redirects to a URL. + */ signOut(options?: { redirectUrl?: URL | string } & ExtraOptions): Promise, signOut(options?: { redirectUrl?: URL | string }): Promise, @@ -136,6 +139,9 @@ export type AuthLike = { * It will be automatically refreshed when it expires. */ getAccessToken(options?: {} & ExtraOptions): Promise, + /** + * React hook that returns the current access token, or null if not signed in. + */ useAccessToken(options?: {} & ExtraOptions): string | null, // THIS_LINE_PLATFORM react-like /** @@ -145,6 +151,9 @@ export type AuthLike = { * It should be kept secret and never exposed to the client. */ getRefreshToken(options?: {} & ExtraOptions): Promise, + /** + * React hook that returns the current refresh token, or null if not signed in. + */ useRefreshToken(options?: {} & ExtraOptions): string | null, // THIS_LINE_PLATFORM react-like /** @@ -185,6 +194,9 @@ export type AuthLike = { * ``` */ getAuthHeaders(options?: {} & ExtraOptions): Promise<{ "x-stack-auth": string }>, + /** + * React hook that returns authentication headers for cross-origin requests. + */ useAuthHeaders(options?: {} & ExtraOptions): { "x-stack-auth": string }, // THIS_LINE_PLATFORM react-like /** diff --git a/packages/template/src/lib/stack-app/contact-channels/index.ts b/packages/template/src/lib/stack-app/contact-channels/index.ts index 43d4941259..54f0ebf038 100644 --- a/packages/template/src/lib/stack-app/contact-channels/index.ts +++ b/packages/template/src/lib/stack-app/contact-channels/index.ts @@ -1,23 +1,89 @@ import { ContactChannelsCrud } from "@stackframe/stack-shared/dist/interface/crud/contact-channels"; - +/** + * Represents a user's contact information for authentication. + * + * Basic information about a contact channel, as seen by a user themselves. + * Usually obtained by calling `user.listContactChannels()` or `user.useContactChannels()`. + */ export type ContactChannel = { + /** + * The id of the contact channel as a string. + */ id: string, + + /** + * The value of the contact channel. If type is "email", this is an email address. + */ value: string, + + /** + * The type of the contact channel. Currently always "email". + */ type: 'email', + + /** + * Indicates whether the contact channel is the user's primary contact channel. + * If an email is set to primary, it will be the value on the `user.primaryEmail` field. + */ isPrimary: boolean, + + /** + * Indicates whether the contact channel is verified. + */ isVerified: boolean, + + /** + * Indicates whether the contact channel is used for authentication. + * If set to `true`, the user can use this contact channel with OTP or password to sign in. + */ usedForAuth: boolean, + /** + * Sends a verification email to this contact channel. + * Once the user clicks the verification link in the email, the contact channel will be marked as verified. + * + * @param options - Optional parameters + * @param options.callbackUrl - URL to redirect to after verification + */ sendVerificationEmail(options?: { callbackUrl?: string }): Promise, + + /** + * Updates the contact channel. + * After updating the value, the contact channel will be marked as unverified. + * + * @param data - The properties to update + */ update(data: ContactChannelUpdateOptions): Promise, + + /** + * Deletes the contact channel. + */ delete(): Promise, } +/** + * Options for creating a new contact channel. + */ export type ContactChannelCreateOptions = { + /** + * The value of the contact channel (e.g., email address). + */ value: string, + + /** + * The type of contact channel. Currently always "email". + */ type: 'email', + + /** + * Whether this contact channel can be used for authentication. + */ usedForAuth: boolean, + + /** + * Whether this should be set as the user's primary contact channel. + */ isPrimary?: boolean, } @@ -31,9 +97,23 @@ export function contactChannelCreateOptionsToCrud(userId: string, options: Conta }; } +/** + * Options for updating a contact channel. + */ export type ContactChannelUpdateOptions = { + /** + * Whether this contact channel can be used for authentication. + */ usedForAuth?: boolean, + + /** + * The new value of the contact channel. + */ value?: string, + + /** + * Whether this should be set as the user's primary contact channel. + */ isPrimary?: boolean, } @@ -45,10 +125,31 @@ export function contactChannelUpdateOptionsToCrud(options: ContactChannelUpdateO }; } +/** + * Like `ContactChannel`, but includes additional methods and properties that require the `SECRET_SERVER_KEY`. + * + * Usually obtained by calling `serverUser.listContactChannels()` or `serverUser.useContactChannels()`. + */ export type ServerContactChannel = ContactChannel & { + /** + * Updates the contact channel. + * + * This method is similar to the one on `ContactChannel`, but also allows setting the `isVerified` property. + * + * @param data - The properties to update + */ update(data: ServerContactChannelUpdateOptions): Promise, } + +/** + * Options for updating a contact channel on the server. + * Extends `ContactChannelUpdateOptions` with server-only properties. + */ export type ServerContactChannelUpdateOptions = ContactChannelUpdateOptions & { + /** + * Whether the contact channel should be marked as verified. + * Only available in server-side operations. + */ isVerified?: boolean, } @@ -61,7 +162,15 @@ export function serverContactChannelUpdateOptionsToCrud(options: ServerContactCh }; } +/** + * Options for creating a new contact channel on the server. + * Extends `ContactChannelCreateOptions` with server-only properties. + */ export type ServerContactChannelCreateOptions = ContactChannelCreateOptions & { + /** + * Whether the contact channel should be marked as verified upon creation. + * Only available in server-side operations. + */ isVerified?: boolean, } export function serverContactChannelCreateOptionsToCrud(userId: string, options: ServerContactChannelCreateOptions): ContactChannelsCrud["Server"]["Create"] { diff --git a/packages/template/src/lib/stack-app/customers/index.ts b/packages/template/src/lib/stack-app/customers/index.ts index 1fe1d8124a..ba6523d087 100644 --- a/packages/template/src/lib/stack-app/customers/index.ts +++ b/packages/template/src/lib/stack-app/customers/index.ts @@ -4,30 +4,47 @@ import { AsyncStoreProperty } from "../common"; export type InlineProduct = yup.InferType; +/** + * Represents a quantifiable resource (credits, API calls, storage, etc.). + */ export type Item = { + /** + * The human-readable name of the item. + */ displayName: string, + /** - * May be negative. + * The current quantity of the item. Can be negative for overdrafts. */ quantity: number, + /** - * Equal to Math.max(0, quantity). + * The quantity clamped to minimum 0. Useful for UI display where negative values don't make sense. */ nonNegativeQuantity: number, }; +/** + * Server-side item with quantity management methods. + */ export type ServerItem = Item & { + /** + * Increases the item quantity by the specified amount. + */ increaseQuantity(amount: number): Promise, + /** - * Decreases the quantity by the given amount. + * Decreases the item quantity by the specified amount. * - * Note that you may want to use tryDecreaseQuantity instead, as it will prevent the quantity from going below 0 in a race-condition-free way. + * Note: Consider using tryDecreaseQuantity instead to prevent race conditions when going below 0. */ decreaseQuantity(amount: number): Promise, + /** - * Decreases the quantity by the given amount and returns true if the result is non-negative; returns false and does nothing if the result would be negative. + * Decreases the quantity by the specified amount only if the result would be non-negative. + * Returns true if successful, false if it would result in negative quantity. * - * Most useful for pre-paid credits. + * Most useful for pre-paid credits to prevent overdrafts. */ tryDecreaseQuantity(amount: number): Promise, }; @@ -107,10 +124,19 @@ export type CustomerPaymentMethodSetupIntent = { stripeAccountId: string, }; +/** + * Payment and item management functionality shared between users and teams. + */ export type Customer = & { + /** + * The unique identifier for the customer (user ID or team ID). + */ readonly id: string, + /** + * Creates a secure checkout URL for purchasing a product via Stripe. + */ createCheckoutUrl(options: ( | { productId: string, returnUrl?: string } | (IsServer extends true ? { product: InlineProduct, returnUrl?: string } : never) diff --git a/packages/template/src/lib/stack-app/index.ts b/packages/template/src/lib/stack-app/index.ts index f3aa76251e..a8334732f9 100644 --- a/packages/template/src/lib/stack-app/index.ts +++ b/packages/template/src/lib/stack-app/index.ts @@ -41,11 +41,23 @@ export type { HandlerUrls, OAuthScopesOnSignIn, ResolvedHandlerUrls } from "./common"; +export type { + ApiKey, TeamApiKey, + TeamApiKeyFirstView, UserApiKey, + UserApiKeyFirstView +} from "./api-keys"; + export type { Connection, OAuthConnection } from "./connected-accounts"; +export type { + Customer, + Item, + ServerItem +} from "./customers"; + export type { ContactChannel, ServerContactChannel diff --git a/packages/template/src/lib/stack-app/teams/index.ts b/packages/template/src/lib/stack-app/teams/index.ts index 11ba2a4fdd..8ab9c93a1c 100644 --- a/packages/template/src/lib/stack-app/teams/index.ts +++ b/packages/template/src/lib/stack-app/teams/index.ts @@ -67,18 +67,31 @@ export type ReceivedTeamInvitation = { } export type Team = { + /** The unique identifier for this team. */ id: string, + /** The display name of the team. */ displayName: string, + /** URL of the team's profile image, or null if not set. */ profileImageUrl: string | null, + /** Custom metadata that can be read and written by the client. */ clientMetadata: any, + /** Custom metadata that can only be read by the client (set via server). */ clientReadOnlyMetadata: any, + /** Invites a user to join the team by email. */ inviteUser(options: { email: string, callbackUrl?: string }): Promise, + /** Lists all users who are members of this team. */ listUsers(): Promise, + /** React hook to get all users who are members of this team. */ useUsers(): TeamUser[], // THIS_LINE_PLATFORM react-like + /** Lists all pending invitations for this team. */ listInvitations(): Promise, + /** React hook to get all pending invitations for this team. */ useInvitations(): SentTeamInvitation[], // THIS_LINE_PLATFORM react-like + /** Updates the team's properties. */ update(update: TeamUpdateOptions): Promise, + /** Deletes the team. */ delete(): Promise, + /** Creates a new API key for this team. */ createApiKey(options: ApiKeyCreationOptions<"team">): Promise, } & AsyncStoreProperty<"apiKeys", [], TeamApiKey[], true> & Customer; diff --git a/packages/template/src/lib/stack-app/users/index.ts b/packages/template/src/lib/stack-app/users/index.ts index c1ec40a734..6075df6faf 100644 --- a/packages/template/src/lib/stack-app/users/index.ts +++ b/packages/template/src/lib/stack-app/users/index.ts @@ -70,6 +70,25 @@ export type ServerOAuthProvider = { */ export type Auth = AuthLike<{}> & { readonly _internalSession: InternalSession, + + /** + * The current user's session, providing access to authentication tokens. + * + * @example-client + * ```tsx + * const user = useUser(); + * // In React components, use the hook for reactive updates + * const { accessToken } = user.currentSession.useTokens(); + * // Or use the async version + * const tokens = await user.currentSession.getTokens(); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const { accessToken } = await user.currentSession.getTokens(); + * ``` + */ readonly currentSession: { getTokens(): Promise<{ accessToken: string | null, refreshToken: string | null }>, useTokens(): { accessToken: string | null, refreshToken: string | null }, // THIS_LINE_PLATFORM react-like @@ -100,84 +119,606 @@ export type Auth = AuthLike<{}> & { **/ export type BaseUser = { + /** + * The unique identifier of the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * console.log(user.id); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * console.log(user.id); + * ``` + */ readonly id: string, + /** + * The display name of the user. The user can modify this value. + * + * @example-client + * ```tsx + * const user = useUser(); + * return
Hello, {user.displayName ?? 'Guest'}
; + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * console.log(`User: ${user.displayName}`); + * ``` + */ readonly displayName: string | null, /** - * The user's email address. + * The user's primary email address. + * + * @note This might NOT be unique across multiple users, so always use `id` for unique identification. * - * Note: This might NOT be unique across multiple users, so always use `id` for unique identification. + * @example-client + * ```tsx + * const user = useUser(); + * return
Email: {user.primaryEmail ?? 'Not set'}
; + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * if (user.primaryEmail) { + * await sendEmail(user.primaryEmail, 'Welcome!'); + * } + * ``` */ readonly primaryEmail: string | null, + + /** + * Whether the primary email of the user is verified. + * + * @example-client + * ```tsx + * const user = useUser(); + * if (!user.primaryEmailVerified) { + * return
Please verify your email to continue.
; + * } + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * if (!user.primaryEmailVerified) { + * // Send reminder email + * } + * ``` + */ readonly primaryEmailVerified: boolean, + + /** + * The profile image URL of the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * return Profile; + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const avatarUrl = user.profileImageUrl ?? 'https://example.com/default.png'; + * ``` + */ readonly profileImageUrl: string | null, + /** + * The date and time when the user signed up. + * + * @example-client + * ```tsx + * const user = useUser(); + * return
Member since {user.signedUpAt.toLocaleDateString()}
; + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const daysSinceSignup = Math.floor((Date.now() - user.signedUpAt.getTime()) / 86400000); + * ``` + */ readonly signedUpAt: Date, + /** + * Custom metadata that can be read and written by the client. + * + * @note Use this for user preferences or non-sensitive data. For sensitive data, use `serverMetadata` instead. + * + * @example-client + * ```tsx + * const user = useUser(); + * const theme = user.clientMetadata?.theme ?? 'light'; + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const preferences = user.clientMetadata?.preferences ?? {}; + * ``` + */ readonly clientMetadata: any, + + /** + * Read-only metadata that can only be set from the server. + * + * @note Useful for storing data that the client can read but not modify, such as subscription tiers or feature flags. + * + * @example-client + * ```tsx + * const user = useUser(); + * const isPremium = user.clientReadOnlyMetadata?.tier === 'premium'; + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.update({ clientReadOnlyMetadata: { tier: 'premium' } }); + * ``` + */ readonly clientReadOnlyMetadata: any, /** * Whether the user has a password set. + * + * @note Users who signed up via OAuth may not have a password. + * + * @example-client + * ```tsx + * const user = useUser(); + * if (!user.hasPassword) { + * return ; + * } + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const authMethods = user.hasPassword ? ['password'] : []; + * ``` */ readonly hasPassword: boolean, + + /** + * Whether OTP/magic link authentication is enabled for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * return
Magic link login: {user.otpAuthEnabled ? 'Enabled' : 'Disabled'}
; + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * if (user.otpAuthEnabled) { + * // User can sign in via magic link + * } + * ``` + */ readonly otpAuthEnabled: boolean, + + /** + * Whether passkey authentication is enabled for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * return
Passkey login: {user.passkeyAuthEnabled ? 'Enabled' : 'Disabled'}
; + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * if (user.passkeyAuthEnabled) { + * // User can sign in via passkey + * } + * ``` + */ readonly passkeyAuthEnabled: boolean, + /** + * Whether multi-factor authentication is required for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * if (user.isMultiFactorRequired) { + * return
MFA is required for your account.
; + * } + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const securityLevel = user.isMultiFactorRequired ? 'high' : 'standard'; + * ``` + */ readonly isMultiFactorRequired: boolean, + + /** + * Whether the user is an anonymous user. + * + * @note Anonymous users are temporary and should be prompted to create a full account to persist their data. + * + * @example-client + * ```tsx + * const user = useUser(); + * if (user.isAnonymous) { + * return ; + * } + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * if (user.isAnonymous) { + * // Don't send marketing emails to anonymous users + * } + * ``` + */ readonly isAnonymous: boolean, + /** * Whether the user is in restricted state (signed up but hasn't completed onboarding requirements). * For example, if email verification is required but the user hasn't verified their email yet. + * + * @note Restricted users have limited access. Check `restrictedReason` to determine why and guide the user accordingly. + * + * @example-client + * ```tsx + * const user = useUser(); + * if (user.isRestricted) { + * return ; + * } + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * if (user.isRestricted) { + * return new Response('Account restricted', { status: 403 }); + * } + * ``` */ readonly isRestricted: boolean, + /** - * The reason why the user is restricted, e.g., { type: "email_not_verified" }, { type: "anonymous" }, or { type: "restricted_by_administrator" }. - * Null if the user is not restricted. + * The reason why the user is restricted. + * + * Possible values: + * - `{ type: "email_not_verified" }` - User needs to verify their email + * - `{ type: "anonymous" }` - User is anonymous and needs to create an account + * - `{ type: "restricted_by_administrator" }` - Admin has restricted this user + * + * Returns `null` if the user is not restricted. + * + * @example-client + * ```tsx + * const user = useUser(); + * if (user.restrictedReason?.type === 'email_not_verified') { + * return
Please verify your email to continue.
; + * } + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * if (user.restrictedReason?.type === 'restricted_by_administrator') { + * // Log admin restriction event + * } + * ``` */ readonly restrictedReason: RestrictedReason | null, + + /** + * Converts the user object to the format expected by the Stack Auth API. + * + * @note Useful for serializing user data in API responses or caching. + * + * @example-client + * ```tsx + * const user = useUser(); + * const json = user.toClientJson(); + * localStorage.setItem('cachedUser', JSON.stringify(json)); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * return Response.json(user.toClientJson()); + * ``` + */ toClientJson(): CurrentUserCrud["Client"]["Read"], /** - * @deprecated, use contact channel's usedForAuth instead + * Whether email/password authentication is enabled for this user. + * @deprecated Use contact channel's usedForAuth instead */ readonly emailAuthEnabled: boolean, /** - * @deprecated + * List of OAuth providers connected to this user's account. + * @deprecated Use getConnectedAccount() instead */ readonly oauthProviders: readonly { id: string }[], } export type UserExtra = { + /** + * Sets the display name of the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * await user.setDisplayName('John Doe'); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.setDisplayName('John Doe'); + * ``` + */ setDisplayName(displayName: string | null): Promise, - /** @deprecated Use contact channel's sendVerificationEmail instead */ + + /** + * Sends a verification email to the user's primary email address. + * @deprecated Use contact channel's sendVerificationEmail instead + */ sendVerificationEmail(): Promise, + + /** + * Sets the client metadata for the user. + * + * @note This replaces all client metadata. To update specific fields, merge with existing metadata first. + * + * @example-client + * ```tsx + * const user = useUser(); + * await user.setClientMetadata({ + * ...user.clientMetadata, + * theme: 'dark', + * language: 'en', + * }); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.setClientMetadata({ onboardingComplete: true }); + * ``` + */ setClientMetadata(metadata: any): Promise, + + /** + * Updates the user's password. Requires the current password for verification. + * + * @note Use this when the user knows their current password. For password reset flows, use `setPassword` instead. + * + * @example-client + * ```tsx + * const user = useUser(); + * const result = await user.updatePassword({ + * oldPassword: 'current-password', + * newPassword: 'new-secure-password', + * }); + * if (result instanceof KnownErrors.PasswordConfirmationMismatch) { + * alert('Current password is incorrect'); + * } + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.updatePassword({ + * oldPassword: 'current-password', + * newPassword: 'new-secure-password', + * }); + * ``` + */ updatePassword(options: { oldPassword: string, newPassword: string}): Promise, + + /** + * Sets a password for the user without requiring the current password. + * + * @note Use this for users who signed up via OAuth and don't have a password yet, or after verifying identity through another method (e.g., email reset link). + * + * @example-client + * ```tsx + * const user = useUser(); + * const result = await user.setPassword({ password: 'new-secure-password' }); + * if (result instanceof KnownErrors.PasswordRequirementsNotMet) { + * alert('Password does not meet requirements'); + * } + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.setPassword({ password: 'new-secure-password' }); + * ``` + */ setPassword(options: { password: string }): Promise, /** - * A shorthand method to update multiple fields of the user at once. + * Updates multiple fields of the user at once. + * + * @example-client + * ```tsx + * const user = useUser(); + * await user.update({ + * displayName: 'New Name', + * clientMetadata: { theme: 'dark' }, + * }); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.update({ + * displayName: 'New Name', + * clientMetadata: { theme: 'dark' }, + * }); + * ``` */ update(update: UserUpdateOptions): Promise, + /** + * React hook to get all contact channels for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * const contactChannels = user.useContactChannels(); + * return ( + *
    + * {contactChannels.map(channel => ( + *
  • {channel.value}
  • + * ))} + *
+ * ); + * ``` + */ useContactChannels(): ContactChannel[], // THIS_LINE_PLATFORM react-like + /** + * Lists all contact channels for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * const channels = await user.listContactChannels(); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const channels = await user.listContactChannels(); + * ``` + */ listContactChannels(): Promise, + /** + * Creates a new contact channel for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * const channel = await user.createContactChannel({ + * type: 'email', + * value: 'backup@example.com', + * }); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const channel = await user.createContactChannel({ + * type: 'email', + * value: 'backup@example.com', + * }); + * ``` + */ createContactChannel(data: ContactChannelCreateOptions): Promise, + /** + * React hook to get all notification categories. + * + * @example-client + * ```tsx + * const user = useUser(); + * const categories = user.useNotificationCategories(); + * return ( + *
    + * {categories.map(cat => ( + *
  • {cat.displayName}
  • + * ))} + *
+ * ); + * ``` + */ useNotificationCategories(): NotificationCategory[], // THIS_LINE_PLATFORM react-like + /** + * Lists all notification categories. + * + * @example-client + * ```tsx + * const user = useUser(); + * const categories = await user.listNotificationCategories(); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const categories = await user.listNotificationCategories(); + * ``` + */ listNotificationCategories(): Promise, + /** + * Deletes the user account. + * + * @example-client + * ```tsx + * const user = useUser(); + * await user.delete(); + * // User is now signed out and account is deleted + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.delete(); + * ``` + */ delete(): Promise, - /** @deprecated Use `getOrLinkConnectedAccount` for redirect behavior, or `getConnectedAccount({ provider, providerAccountId })` for existence check. */ + /** + * Gets an OAuth connected account for the user. + * + * @deprecated Use `getOrLinkConnectedAccount` for redirect behavior, or `getConnectedAccount({ provider, providerAccountId })` for existence check. + * + * @example-client + * ```tsx + * const user = useUser(); + * const googleAccount = await user.getConnectedAccount('google', { + * or: 'redirect', + * scopes: ['https://www.googleapis.com/auth/calendar'], + * }); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const googleAccount = await user.getConnectedAccount('google'); + * ``` + */ getConnectedAccount(id: ProviderType, options: { or: 'redirect', scopes?: string[] }): Promise, /** @deprecated Use `getConnectedAccount({ provider, providerAccountId })` for existence check, or `getOrLinkConnectedAccount` for redirect behavior. */ getConnectedAccount(id: ProviderType, options?: { or?: 'redirect' | 'throw' | 'return-null', scopes?: string[] }): Promise, /** Get a specific connected account by provider and providerAccountId. Returns null if not found. */ getConnectedAccount(account: { provider: string, providerAccountId: string }): Promise, + /** + * React hook to get an OAuth connected account for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * const googleAccount = user.useConnectedAccount('google', { + * or: 'redirect', + * scopes: ['https://www.googleapis.com/auth/calendar'], + * }); + * // Use googleAccount.accessToken to call Google APIs + * ``` + */ // IF_PLATFORM react-like /** @deprecated Use `useOrLinkConnectedAccount` for redirect behavior, or `useConnectedAccount({ provider, providerAccountId })` for existence check. */ useConnectedAccount(id: ProviderType, options: { or: 'redirect', scopes?: string[] }): DeprecatedOAuthConnection, @@ -198,26 +739,167 @@ export type UserExtra = { /** React hook: get a connected account for the given provider, or redirect to link one if none exists or the token/scopes are insufficient. */ useOrLinkConnectedAccount(provider: string, options?: { scopes?: string[] }): OAuthConnection, // THIS_LINE_PLATFORM react-like + /** + * Checks if the user has a specific permission. + * + * @example-client + * ```tsx + * const user = useUser(); + * const canEdit = await user.hasPermission(team, 'edit'); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const canEdit = await user.hasPermission(team, 'edit'); + * ``` + */ hasPermission(scope: Team, permissionId: string): Promise, hasPermission(permissionId: string): Promise, + /** + * Gets a specific permission for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * const permission = await user.getPermission(team, 'admin'); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const permission = await user.getPermission(team, 'admin'); + * ``` + */ getPermission(scope: Team, permissionId: string): Promise, getPermission(permissionId: string): Promise, + /** + * Lists all permissions for the user in a given scope. + * + * @example-client + * ```tsx + * const user = useUser(); + * const permissions = await user.listPermissions(team, { recursive: true }); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const permissions = await user.listPermissions(team); + * ``` + */ listPermissions(scope: Team, options?: { recursive?: boolean }): Promise, listPermissions(options?: { recursive?: boolean }): Promise, // IF_PLATFORM react-like + /** + * React hook to get all permissions for the user in a given scope. + * + * @example-client + * ```tsx + * const user = useUser(); + * const permissions = user.usePermissions(team); + * return ( + *
    + * {permissions.map(p =>
  • {p.id}
  • )} + *
+ * ); + * ``` + */ usePermissions(scope: Team, options?: { recursive?: boolean }): TeamPermission[], usePermissions(options?: { recursive?: boolean }): TeamPermission[], + /** + * React hook to get a specific permission for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * const adminPermission = user.usePermission(team, 'admin'); + * if (adminPermission) { + * return ; + * } + * ``` + */ usePermission(scope: Team, permissionId: string): TeamPermission | null, usePermission(permissionId: string): TeamPermission | null, // END_PLATFORM + /** + * The currently selected team for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * if (user.selectedTeam) { + * return
Current team: {user.selectedTeam.displayName}
; + * } + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const teamId = user.selectedTeam?.id; + * ``` + */ readonly selectedTeam: Team | null, + + /** + * Sets the selected team for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * await user.setSelectedTeam(team); + * // Or by ID + * await user.setSelectedTeam('team-id'); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.setSelectedTeam('team-id'); + * ``` + */ setSelectedTeam(teamOrId: string | Team | null): Promise, + + /** + * Creates a new team with the user as a member. + * + * @example-client + * ```tsx + * const user = useUser(); + * const team = await user.createTeam({ + * displayName: 'My Team', + * }); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const team = await user.createTeam({ + * displayName: 'My Team', + * }); + * ``` + */ createTeam(data: TeamCreateOptions): Promise, + /** + * Removes the user from the specified team. + * + * @example-client + * ```tsx + * const user = useUser(); + * await user.leaveTeam(team); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.leaveTeam(team); + * ``` + */ leaveTeam(team: Team): Promise, /** @@ -245,19 +927,175 @@ export type UserExtra = { */ useTeamInvitations(): ReceivedTeamInvitation[], // THIS_LINE_PLATFORM react-like + /** + * Gets all active sessions for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * const sessions = await user.getActiveSessions(); + * return ( + *
    + * {sessions.map(s =>
  • {s.device}
  • )} + *
+ * ); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const sessions = await user.getActiveSessions(); + * ``` + */ getActiveSessions(): Promise, + /** + * Revokes a specific session for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * await user.revokeSession(sessionId); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.revokeSession(sessionId); + * ``` + */ revokeSession(sessionId: string): Promise, + /** + * Gets the user's profile within a specific team. + * + * @example-client + * ```tsx + * const user = useUser(); + * const profile = await user.getTeamProfile(team); + * console.log(profile.displayName); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const profile = await user.getTeamProfile(team); + * ``` + */ getTeamProfile(team: Team): Promise, + /** + * React hook to get the user's profile within a specific team. + * + * @example-client + * ```tsx + * const user = useUser(); + * const profile = user.useTeamProfile(team); + * return
Team name: {profile.displayName}
; + * ``` + */ useTeamProfile(team: Team): EditableTeamMemberProfile, // THIS_LINE_PLATFORM react-like + /** + * Creates a new API key for the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * const apiKey = await user.createApiKey({ + * description: 'My API key', + * expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days + * }); + * // Save apiKey.secretApiKey - it won't be shown again + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const apiKey = await user.createApiKey({ + * description: 'Server-created key', + * }); + * ``` + */ createApiKey(options: ApiKeyCreationOptions<"user">): Promise, + /** + * React hook to get all OAuth providers connected to the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * const providers = user.useOAuthProviders(); + * return ( + *
    + * {providers.map(p =>
  • {p.type}
  • )} + *
+ * ); + * ``` + */ useOAuthProviders(): OAuthProvider[], // THIS_LINE_PLATFORM react-like + /** + * Lists all OAuth providers connected to the user. + * + * @example-client + * ```tsx + * const user = useUser(); + * const providers = await user.listOAuthProviders(); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const providers = await user.listOAuthProviders(); + * ``` + */ listOAuthProviders(): Promise, + /** + * React hook to get a specific OAuth provider by ID. + * + * @example-client + * ```tsx + * const user = useUser(); + * const googleProvider = user.useOAuthProvider('google'); + * if (googleProvider) { + * return
Connected to Google
; + * } + * ``` + */ useOAuthProvider(id: string): OAuthProvider | null, // THIS_LINE_PLATFORM react-like + /** + * Gets a specific OAuth provider by ID. + * + * @example-client + * ```tsx + * const user = useUser(); + * const googleProvider = await user.getOAuthProvider('google'); + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * const googleProvider = await user.getOAuthProvider('google'); + * ``` + */ getOAuthProvider(id: string): Promise, + /** + * Registers a passkey for the user for passwordless authentication. + * + * @example-client + * ```tsx + * const user = useUser(); + * const result = await user.registerPasskey(); + * if (result.status === 'ok') { + * alert('Passkey registered successfully!'); + * } + * ``` + * + * @example-server + * ```ts + * const user = await stackServerApp.getUser(); + * await user.registerPasskey(); + * ``` + */ registerPasskey(options?: { hostname?: string }): Promise>, } & AsyncStoreProperty<"apiKeys", [], UserApiKey[], true> @@ -346,19 +1184,42 @@ export function userUpdateOptionsToCrud(options: UserUpdateOptions): CurrentUser export type ServerBaseUser = { + /** + * Sets the primary email for the user (server-side only). + */ setPrimaryEmail(email: string | null, options?: { verified?: boolean | undefined }): Promise, + /** + * The date and time when the user was last active. + */ readonly lastActiveAt: Date, + /** + * Server-only metadata that can only be read and written from the server. + */ readonly serverMetadata: any, + /** + * Sets the server metadata for the user. + */ setServerMetadata(metadata: any): Promise, + /** + * Sets the client read-only metadata that clients can read but not write. + */ setClientReadOnlyMetadata(metadata: any): Promise, - /** Whether the user is restricted by an administrator. Can be set manually or by sign-up rules. */ + /** + * Whether the user is restricted by an administrator. Can be set manually or by sign-up rules. + */ readonly restrictedByAdmin: boolean, - /** Public reason shown to the user explaining why they are restricted. Optional. */ + + /** + * Public reason shown to the user explaining why they are restricted. Optional. + */ readonly restrictedByAdminReason: string | null, - /** Private details about the restriction (e.g., which sign-up rule triggered). Only visible to server access and above. */ + + /** + * Private details about the restriction (e.g., which sign-up rule triggered). Only visible to server access and above. + */ readonly restrictedByAdminPrivateDetails: string | null, /** Best-effort ISO country code captured at sign-up time from request geo headers. */ readonly countryCode: string | null, @@ -370,20 +1231,44 @@ export type ServerBaseUser = { }, }, + /** + * Creates a new team (server-side only). + */ createTeam(data: Omit): Promise, + /** + * React hook to get all contact channels for the user (server version). + */ useContactChannels(): ServerContactChannel[], // THIS_LINE_PLATFORM react-like + /** + * Lists all contact channels for the user (server version). + */ listContactChannels(): Promise, + /** + * Creates a new contact channel for the user (server-side only). + */ createContactChannel(data: ServerContactChannelCreateOptions): Promise, + /** + * Updates the user's information (server-side only). + */ update(user: ServerUserUpdateOptions): Promise, + /** + * Grants a permission to the user (server-side only). + */ grantPermission(scope: Team, permissionId: string): Promise, grantPermission(permissionId: string): Promise, + /** + * Revokes a permission from the user (server-side only). + */ revokePermission(scope: Team, permissionId: string): Promise, revokePermission(permissionId: string): Promise, + /** + * Gets a specific permission for the user (server version). + */ getPermission(scope: Team, permissionId: string): Promise, getPermission(permissionId: string): Promise, @@ -394,21 +1279,39 @@ export type ServerBaseUser = { listPermissions(options?: { recursive?: boolean }): Promise, // IF_PLATFORM react-like + /** + * React hook to get all permissions for the user in a given scope (server version). + */ usePermissions(scope: Team, options?: { recursive?: boolean }): TeamPermission[], usePermissions(options?: { recursive?: boolean }): TeamPermission[], + /** + * React hook to get a specific permission for the user (server version). + */ usePermission(scope: Team, permissionId: string): TeamPermission | null, usePermission(permissionId: string): TeamPermission | null, // END_PLATFORM + /** + * React hook to get all OAuth providers connected to the user (server version). + */ useOAuthProviders(): ServerOAuthProvider[], // THIS_LINE_PLATFORM react-like + /** + * Lists all OAuth providers connected to the user (server version). + */ listOAuthProviders(): Promise, + /** + * React hook to get a specific OAuth provider by ID (server version). + */ useOAuthProvider(id: string): ServerOAuthProvider | null, // THIS_LINE_PLATFORM react-like + /** + * Gets a specific OAuth provider by ID (server version). + */ getOAuthProvider(id: string): Promise, /** - * Creates a new session object with a refresh token for this user. Can be used to impersonate them. + * Creates a new session for the user. Can be used for impersonation. */ createSession(options?: { expiresInMillis?: number, isImpersonation?: boolean }): Promise<{ getTokens(): Promise<{ accessToken: string | null, refreshToken: string | null }>,