Compare commits

..

46 Commits

Author SHA1 Message Date
Waleed
2bb68335ee v0.5.79: longer MCP tools timeout, optimize loop/parallel regeneration, enrich.so integration 2026-01-31 21:57:56 -08:00
Waleed
8528fbe2d2 v0.5.78: billing fixes, mcp timeout increase, reactquery migrations, updated tool param visibilities, DSPy and Google Maps integrations 2026-01-31 13:48:22 -08:00
Waleed
31fdd2be13 v0.5.77: room manager redis migration, tool outputs, ui fixes 2026-01-30 14:57:17 -08:00
Waleed
028bc652c2 v0.5.76: posthog improvements, readme updates 2026-01-29 00:13:19 -08:00
Waleed
c6bf5cd58c v0.5.75: search modal overhaul, helm chart updates, run from block, terminal and visual debugging improvements 2026-01-28 22:54:13 -08:00
Vikhyath Mondreti
11dc18a80d v0.5.74: autolayout improvements, clerk integration, auth enforcements 2026-01-27 20:37:39 -08:00
Waleed
ab4e9dc72f v0.5.73: ci, helm updates, kb, ui fixes, note block enhancements 2026-01-26 22:04:35 -08:00
Vikhyath Mondreti
1c58c35bd8 v0.5.72: azure connection string, supabase improvement, multitrigger resolution, docs quick reference 2026-01-25 23:42:27 -08:00
Waleed
d63a5cb504 v0.5.71: ux, ci improvements, docs updates 2026-01-25 03:08:08 -08:00
Waleed
8bd5d41723 v0.5.70: router fix, anthropic agent response format adherence 2026-01-24 20:57:02 -08:00
Waleed
c12931bc50 v0.5.69: kb upgrades, blog, copilot improvements, auth consolidation (#2973)
* fix(subflows): tag dropdown + resolution logic (#2949)

* fix(subflows): tag dropdown + resolution logic

* fixes;

* revert parallel change

* chore(deps): bump posthog-js to 1.334.1 (#2948)

* fix(idempotency): add conflict target to atomicallyClaimDb query + remove redundant db namespace tracking (#2950)

* fix(idempotency): add conflict target to atomicallyClaimDb query

* delete needs to account for namespace

* simplify namespace filtering logic

* fix cleanup

* consistent target

* improvement(kb): add document filtering, select all, and React Query migration (#2951)

* improvement(kb): add document filtering, select all, and React Query migration

* test(kb): update tests for enabledFilter and removed userId params

* fix(kb): remove non-null assertion, add explicit guard

* improvement(logs): trace span, details (#2952)

* improvement(action-bar): ordering

* improvement(logs): details, trace span

* feat(blog): v0.5 release post (#2953)

* feat(blog): v0.5 post

* improvement(blog): simplify title and remove code block header

- Simplified blog title from Introducing Sim Studio v0.5 to Introducing Sim v0.5
- Removed language label header and copy button from code blocks for cleaner appearance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ack PR comments

* small styling improvements

* created system to create post-specific components

* updated componnet

* cache invalidation

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(admin): add credits endpoint to issue credits to users (#2954)

* feat(admin): add credits endpoint to issue credits to users

* fix(admin): use existing credit functions and handle enterprise seats

* fix(admin): reject NaN and Infinity in amount validation

* styling

* fix(admin): validate userId and email are strings

* improvement(copilot): fast mode, subagent tool responses and allow preferences (#2955)

* Improvements

* Fix actions mapping

* Remove console logs

* fix(billing): handle missing userStats and prevent crashes (#2956)

* fix(billing): handle missing userStats and prevent crashes

* fix(billing): correct import path for getFilledPillColor

* fix(billing): add Number.isFinite check to lastPeriodCost

* fix(logs): refresh logic to refresh logs details (#2958)

* fix(security): add authentication and input validation to API routes (#2959)

* fix(security): add authentication and input validation to API routes

* moved utils

* remove extraneous commetns

* removed unused dep

* improvement(helm): add internal ingress support and same-host path consolidation (#2960)

* improvement(helm): add internal ingress support and same-host path consolidation

* improvement(helm): clean up ingress template comments

Simplify verbose inline Helm comments and section dividers to match the
minimal style used in services.yaml.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(helm): add missing copilot path consolidation for realtime host

When copilot.host equals realtime.host but differs from app.host,
copilot paths were not being routed. Added logic to consolidate
copilot paths into the realtime rule for this scenario.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* improvement(helm): follow ingress best practices

- Remove orphan comments that appeared when services were disabled
- Add documentation about path ordering requirements
- Paths rendered in order: realtime, copilot, app (specific before catch-all)
- Clean template output matching industry Helm chart standards

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(blog): enterprise post (#2961)

* feat(blog): enterprise post

* added more images, styling

* more content

* updated v0-5 post

* remove unused transition

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>

* fix(envvars): resolution standardized (#2957)

* fix(envvars): resolution standardized

* remove comments

* address bugbot

* fix highlighting for env vars

* remove comments

* address greptile

* address bugbot

* fix(copilot): mask credentials fix (#2963)

* Fix copilot masking

* Clean up

* Lint

* improvement(webhooks): remove dead code (#2965)

* fix(webhooks): subscription recreation path

* improvement(webhooks): remove dead code

* fix tests

* address bugbot comments

* fix restoration edge case

* fix more edge cases

* address bugbot comments

* fix gmail polling

* add warnings for UI indication for credential sets

* fix(preview): subblock values (#2969)

* fix(child-workflow): nested spans handoff (#2966)

* fix(child-workflow): nested spans handoff

* remove overly defensive programming

* update type check

* type more code

* remove more dead code

* address bugbot comments

* fix(security): restrict API key access on internal-only routes (#2964)

* fix(security): restrict API key access on internal-only routes

* test(security): update function execute tests for checkInternalAuth

* updated agent handler

* move session check higher in checkSessionOrInternalAuth

* extracted duplicate code into helper for resolving user from jwt

* fix(copilot): update copilot chat title (#2968)

* fix(hitl): fix condition blocks after hitl (#2967)

* fix(notes): ghost edges (#2970)

* fix(notes): ghost edges

* fix deployed state fallback

* fallback

* remove UI level checks

* annotation missing from autoconnect source check

* improvement(docs): loop and parallel var reference syntax (#2975)

* fix(blog): slash actions description (#2976)

* improvement(docs): loop and parallel var reference syntax

* fix(blog): slash actions description

* fix(auth): copilot routes (#2977)

* Fix copilot auth

* Fix

* Fix

* Fix

* fix(copilot): fix edit summary for loops/parallels (#2978)

* fix(integrations): hide from tool bar (#2544)

* fix(landing): ui (#2979)

* fix(edge-validation): race condition on collaborative add (#2980)

* fix(variables): boolean type support and input improvements (#2981)

* fix(variables): boolean type support and input improvements

* fix formatting

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-01-24 14:29:53 -08:00
Waleed
e9c4251c1c v0.5.68: router block reasoning, executor improvements, variable resolution consolidation, helm updates (#2946)
* improvement(workflow-item): stabilize avatar layout and fix name truncation (#2939)

* improvement(workflow-item): stabilize avatar layout and fix name truncation

* fix(avatars): revert overflow bg to hardcoded color for contrast

* fix(executor): stop parallel execution when block errors (#2940)

* improvement(helm): add per-deployment extraVolumes support (#2942)

* fix(gmail): expose messageId field in read email block (#2943)

* fix(resolver): consolidate reference resolution  (#2941)

* fix(resolver): consolidate code to resolve references

* fix edge cases

* use already formatted error

* fix multi index

* fix backwards compat reachability

* handle backwards compatibility accurately

* use shared constant correctly

* feat(router): expose reasoning output in router v2 block (#2945)

* fix(copilot): always allow, credential masking (#2947)

* Fix always allow, credential validation

* Credential masking

* Autoload

* fix(executor): handle condition dead-end branches in loops (#2944)

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
2026-01-22 13:48:15 -08:00
Waleed
cc2be33d6b v0.5.67: loading, password reset, ui improvements, helm updates (#2928)
* fix(zustand): updated to useShallow from deprecated createWithEqualityFn (#2919)

* fix(logger): use direct env access for webpack inlining (#2920)

* fix(notifications): text overflow with line-clamp (#2921)

* chore(helm): add env vars for Vertex AI, orgs, and telemetry (#2922)

* fix(auth): improve reset password flow and consolidate brand detection (#2924)

* fix(auth): improve reset password flow and consolidate brand detection

* fix(auth): set errorHandled for EMAIL_NOT_VERIFIED to prevent duplicate error

* fix(auth): clear success message on login errors

* chore(auth): fix import order per lint

* fix(action-bar): duplicate subflows with children (#2923)

* fix(action-bar): duplicate subflows with children

* fix(action-bar): add validateTriggerPaste for subflow duplicate

* fix(resolver): agent response format, input formats, root level (#2925)

* fix(resolvers): agent response format, input formats, root level

* fix response block initial seeding

* fix tests

* fix(messages-input): fix cursor alignment and auto-resize with overlay (#2926)

* fix(messages-input): fix cursor alignment and auto-resize with overlay

* fixed remaining zustand warnings

* fix(stores): remove dead code causing log spam on startup (#2927)

* fix(stores): remove dead code causing log spam on startup

* fix(stores): replace custom tools zustand store with react query cache

* improvement(ui): use BrandedButton and BrandedLink components (#2930)

- Refactor auth forms to use BrandedButton component
- Add BrandedLink component for changelog page
- Reduce code duplication in login, signup, reset-password forms
- Update star count default value

* fix(custom-tools): remove unsafe title fallback in getCustomTool (#2929)

* fix(custom-tools): remove unsafe title fallback in getCustomTool

* fix(custom-tools): restore title fallback in getCustomTool lookup

Custom tools are referenced by title (custom_${title}), not database ID.
The title fallback is required for client-side tool resolution to work.

* fix(null-bodies): empty bodies handling (#2931)

* fix(null-statuses): empty bodies handling

* address bugbot comment

* fix(token-refresh): microsoft, notion, x, linear (#2933)

* fix(microsoft): proactive refresh needed

* fix(x): missing token refresh flag

* notion and linear missing flag too

* address bugbot comment

* fix(auth): handle EMAIL_NOT_VERIFIED in onError callback (#2932)

* fix(auth): handle EMAIL_NOT_VERIFIED in onError callback

* refactor(auth): extract redirectToVerify helper to reduce duplication

* fix(workflow-selector): use dedicated selector for workflow dropdown (#2934)

* feat(workflow-block): preview (#2935)

* improvement(copilot): tool configs to show nested props (#2936)

* fix(auth): add genericOAuth providers to trustedProviders (#2937)

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
2026-01-21 22:53:25 -08:00
Vikhyath Mondreti
45371e521e v0.5.66: external http requests fix, ring highlighting 2026-01-21 02:55:39 -08:00
Waleed
0ce0f98aa5 v0.5.65: gemini updates, textract integration, ui updates (#2909)
* fix(google): wrap primitive tool responses for Gemini API compatibility (#2900)

* fix(canonical): copilot path + update parent (#2901)

* fix(rss): add top-level title, link, pubDate fields to RSS trigger output (#2902)

* fix(rss): add top-level title, link, pubDate fields to RSS trigger output

* fix(imap): add top-level fields to IMAP trigger output

* improvement(browseruse): add profile id param (#2903)

* improvement(browseruse): add profile id param

* make request a stub since we have directExec

* improvement(executor): upgraded abort controller to handle aborts for loops and parallels (#2880)

* improvement(executor): upgraded abort controller to handle aborts for loops and parallels

* comments

* improvement(files): update execution for passing base64 strings (#2906)

* progress

* improvement(execution): update execution for passing base64 strings

* fix types

* cleanup comments

* path security vuln

* reject promise correctly

* fix redirect case

* remove proxy routes

* fix tests

* use ipaddr

* feat(tools): added textract, added v2 for mistral, updated tag dropdown (#2904)

* feat(tools): added textract

* cleanup

* ack pr comments

* reorder

* removed upload for textract async version

* fix additional fields dropdown in editor, update parser to leave validation to be done on the server

* added mistral v2, files v2, and finalized textract

* updated the rest of the old file patterns, updated mistral outputs for v2

* updated tag dropdown to parse non-operation fields as well

* updated extension finder

* cleanup

* added description for inputs to workflow

* use helper for internal route check

* fix tag dropdown merge conflict change

* remove duplicate code

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>

* fix(ui): change add inputs button to match output selector (#2907)

* fix(canvas): removed invite to workspace from canvas popover (#2908)

* fix(canvas): removed invite to workspace

* removed unused props

* fix(copilot): legacy tool display names (#2911)

* fix(a2a): canonical merge  (#2912)

* fix canonical merge

* fix empty array case

* fix(change-detection): copilot diffs have extra field (#2913)

* improvement(logs): improved logs ui bugs, added subflow disable UI (#2910)

* improvement(logs): improved logs ui bugs, added subflow disable UI

* added duplicate to action bar for subflows

* feat(broadcast): email v0.5 (#2905)

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
2026-01-20 23:54:55 -08:00
Waleed
dff1c9d083 v0.5.64: unsubscribe, search improvements, metrics, additional SSO configuration 2026-01-20 00:34:11 -08:00
Vikhyath Mondreti
b09f683072 v0.5.63: ui and performance improvements, more google tools 2026-01-18 15:22:42 -08:00
Vikhyath Mondreti
a8bb0db660 v0.5.62: webhook bug fixes, seeding default subblock values, block selection fixes 2026-01-16 20:27:06 -08:00
Waleed
af82820a28 v0.5.61: webhook improvements, workflow controls, react query for deployment status, chat fixes, reducto and pulse OCR, linear fixes 2026-01-16 18:06:23 -08:00
Waleed
4372841797 v0.5.60: invitation flow improvements, chat fixes, a2a improvements, additional copilot actions 2026-01-15 00:02:18 -08:00
Waleed
5e8c843241 v0.5.59: a2a support, documentation 2026-01-13 13:21:21 -08:00
Waleed
7bf3d73ee6 v0.5.58: export folders, new tools, permissions groups enhancements 2026-01-13 00:56:59 -08:00
Vikhyath Mondreti
7ffc11a738 v0.5.57: subagents, context menu improvements, bug fixes 2026-01-11 11:38:40 -08:00
Waleed
be578e2ed7 v0.5.56: batch operations, access control and permission groups, billing fixes 2026-01-10 00:31:34 -08:00
Waleed
f415e5edc4 v0.5.55: polling groups, bedrock provider, devcontainer fixes, workflow preview enhancements 2026-01-08 23:36:56 -08:00
Waleed
13a6e6c3fa v0.5.54: seo, model blacklist, helm chart updates, fireflies integration, autoconnect improvements, billing fixes 2026-01-07 16:09:45 -08:00
Waleed
f5ab7f21ae v0.5.53: hotkey improvements, added redis fallback, fixes for workflow tool 2026-01-06 23:34:52 -08:00
Waleed
bfb6fffe38 v0.5.52: new port-based router block, combobox expression and variable support 2026-01-06 16:14:10 -08:00
Waleed
4fbec0a43f v0.5.51: triggers, kb, condition block improvements, supabase and grain integration updates 2026-01-06 14:26:46 -08:00
Waleed
585f5e365b v0.5.50: import improvements, ui upgrades, kb styling and performance improvements 2026-01-05 00:35:55 -08:00
Waleed
3792bdd252 v0.5.49: hitl improvements, new email styles, imap trigger, logs context menu (#2672)
* feat(logs-context-menu): consolidated logs utils and types, added logs record context menu (#2659)

* feat(email): welcome email; improvement(emails): ui/ux (#2658)

* feat(email): welcome email; improvement(emails): ui/ux

* improvement(emails): links, accounts, preview

* refactor(emails): file structure and wrapper components

* added envvar for personal emails sent, added isHosted gate

* fixed failing tests, added env mock

* fix: removed comment

---------

Co-authored-by: waleed <walif6@gmail.com>

* fix(logging): hitl + trigger dev crash protection (#2664)

* hitl gaps

* deal with trigger worker crashes

* cleanup import strcuture

* feat(imap): added support for imap trigger (#2663)

* feat(tools): added support for imap trigger

* feat(imap): added parity, tested

* ack PR comments

* final cleanup

* feat(i18n): update translations (#2665)

Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* fix(grain): updated grain trigger to auto-establish trigger (#2666)

Co-authored-by: aadamgough <adam@sim.ai>

* feat(admin): routes to manage deployments (#2667)

* feat(admin): routes to manage deployments

* fix naming fo deployed by

* feat(time-picker): added timepicker emcn component, added to playground, added searchable prop for dropdown, added more timezones for schedule, updated license and notice date (#2668)

* feat(time-picker): added timepicker emcn component, added to playground, added searchable prop for dropdown, added more timezones for schedule, updated license and notice date

* removed unused params, cleaned up redundant utils

* improvement(invite): aligned styling (#2669)

* improvement(invite): aligned with rest of app

* fix(invite): error handling

* fix: addressed comments

---------

Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com>
Co-authored-by: aadamgough <adam@sim.ai>
2026-01-03 13:19:18 -08:00
Waleed
eb5d1f3e5b v0.5.48: copy-paste workflow blocks, docs updates, mcp tool fixes 2025-12-31 18:00:04 -08:00
Waleed
54ab82c8dd v0.5.47: deploy workflow as mcp, kb chunks tokenizer, UI improvements, jira service management tools 2025-12-30 23:18:58 -08:00
Waleed
f895bf469b v0.5.46: build improvements, greptile, light mode improvements 2025-12-29 02:17:52 -08:00
Waleed
dd3209af06 v0.5.45: light mode fixes, realtime usage indicator, docker build improvements 2025-12-27 19:57:42 -08:00
Waleed
b6ba3b50a7 v0.5.44: keyboard shortcuts, autolayout, light mode, byok, testing improvements 2025-12-26 21:25:19 -08:00
Waleed
b304233062 v0.5.43: export logs, circleback, grain, vertex, code hygiene, schedule improvements 2025-12-23 19:19:18 -08:00
Vikhyath Mondreti
57e4b49bd6 v0.5.42: fix memory migration 2025-12-23 01:24:54 -08:00
Vikhyath Mondreti
e12dd204ed v0.5.41: memory fixes, copilot improvements, knowledgebase improvements, LLM providers standardization 2025-12-23 00:15:18 -08:00
Vikhyath Mondreti
3d9d9cbc54 v0.5.40: supabase ops to allow non-public schemas, jira uuid 2025-12-21 22:28:05 -08:00
Waleed
0f4ec962ad v0.5.39: notion, workflow variables fixes 2025-12-20 20:44:00 -08:00
Waleed
4827866f9a v0.5.38: snap to grid, copilot ux improvements, billing line items 2025-12-20 17:24:38 -08:00
Waleed
3e697d9ed9 v0.5.37: redaction utils consolidation, logs updates, autoconnect improvements, additional kb tag types 2025-12-19 22:31:55 -08:00
Martin Yankov
4431a1a484 fix(helm): add custom egress rules to realtime network policy (#2481)
The realtime service network policy was missing the custom egress rules section
that allows configuration of additional egress rules via values.yaml. This caused
the realtime pods to be unable to connect to external databases (e.g., PostgreSQL
on port 5432) when using external database configurations.

The app network policy already had this section, but the realtime network policy
was missing it, creating an inconsistency and preventing the realtime service
from accessing external databases configured via networkPolicy.egress values.

This fix adds the same custom egress rules template section to the realtime
network policy, matching the app network policy behavior and allowing users to
configure database connectivity via values.yaml.
2025-12-19 18:59:08 -08:00
Waleed
4d1a9a3f22 v0.5.36: hitl improvements, opengraph, slack fixes, one-click unsubscribe, auth checks, new db indexes 2025-12-19 01:27:49 -08:00
Vikhyath Mondreti
eb07a080fb v0.5.35: helm updates, copilot improvements, 404 for docs, salesforce fixes, subflow resize clamping 2025-12-18 16:23:19 -08:00
24 changed files with 30 additions and 1732 deletions

View File

@@ -6,11 +6,9 @@ import { getSession } from '@/lib/auth'
import { refreshOAuthToken } from '@/lib/oauth'
import {
getMicrosoftRefreshTokenExpiry,
getTikTokRefreshTokenExpiry,
isMicrosoftProvider,
isTikTokProvider,
PROACTIVE_REFRESH_THRESHOLD_DAYS,
} from '@/lib/oauth/utils'
} from '@/lib/oauth/microsoft'
const logger = createLogger('OAuthUtilsAPI')
@@ -222,13 +220,13 @@ export async function refreshAccessTokenIfNeeded(
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))
// Check if we should proactively refresh to prevent refresh token expiry
// This applies to providers with expiring refresh tokens (Microsoft: 90 days, TikTok: 365 days)
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
const proactiveRefreshThreshold = new Date(
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
)
const refreshTokenNeedsProactiveRefresh =
!!credential.refreshToken &&
(isMicrosoftProvider(credential.providerId) || isTikTokProvider(credential.providerId)) &&
isMicrosoftProvider(credential.providerId) &&
refreshTokenExpiresAt &&
refreshTokenExpiresAt <= proactiveRefreshThreshold
@@ -273,8 +271,6 @@ export async function refreshAccessTokenIfNeeded(
if (isMicrosoftProvider(credential.providerId)) {
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
} else if (isTikTokProvider(credential.providerId)) {
updateData.refreshTokenExpiresAt = getTikTokRefreshTokenExpiry()
}
// Update the token in the database
@@ -325,13 +321,13 @@ export async function refreshTokenIfNeeded(
(!credential.accessToken || (accessTokenExpiresAt && accessTokenExpiresAt <= now))
// Check if we should proactively refresh to prevent refresh token expiry
// This applies to providers with expiring refresh tokens (Microsoft: 90 days, TikTok: 365 days)
// This applies to Microsoft providers whose refresh tokens expire after 90 days of inactivity
const proactiveRefreshThreshold = new Date(
now.getTime() + PROACTIVE_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
)
const refreshTokenNeedsProactiveRefresh =
!!credential.refreshToken &&
(isMicrosoftProvider(credential.providerId) || isTikTokProvider(credential.providerId)) &&
isMicrosoftProvider(credential.providerId) &&
refreshTokenExpiresAt &&
refreshTokenExpiresAt <= proactiveRefreshThreshold
@@ -372,8 +368,6 @@ export async function refreshTokenIfNeeded(
if (isMicrosoftProvider(credential.providerId)) {
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
} else if (isTikTokProvider(credential.providerId)) {
updateData.refreshTokenExpiresAt = getTikTokRefreshTokenExpiry()
}
await db.update(account).set(updateData).where(eq(account.id, credentialId))

View File

@@ -1,70 +0,0 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { getBaseUrl } from '@/lib/core/utils/urls'
const logger = createLogger('TikTokAuthorize')
export const dynamic = 'force-dynamic'
export async function GET(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const clientKey = env.TIKTOK_CLIENT_ID
if (!clientKey) {
logger.error('TIKTOK_CLIENT_ID not configured')
return NextResponse.json({ error: 'TikTok client key not configured' }, { status: 500 })
}
// Get the return URL from query params or use default
const searchParams = request.nextUrl.searchParams
const returnUrl = searchParams.get('returnUrl') || `${getBaseUrl()}/workspace`
const baseUrl = getBaseUrl()
const redirectUri = `${baseUrl}/api/auth/tiktok/callback`
// Generate a random state for CSRF protection
const state = Buffer.from(
JSON.stringify({
returnUrl,
timestamp: Date.now(),
})
).toString('base64url')
// TikTok scopes
const scopes = [
'user.info.basic',
'user.info.profile',
'user.info.stats',
'video.list',
'video.publish',
]
// Build TikTok authorization URL with client_key (not client_id)
// Note: TikTok expects raw commas in scope parameter, not URL-encoded %2C
// So we manually construct the URL to avoid automatic encoding
const scopeString = scopes.join(',')
const encodedRedirectUri = encodeURIComponent(redirectUri)
const encodedState = encodeURIComponent(state)
const authUrl = `https://www.tiktok.com/v2/auth/authorize/?client_key=${clientKey}&response_type=code&scope=${scopeString}&redirect_uri=${encodedRedirectUri}&state=${encodedState}`
logger.info('Redirecting to TikTok authorization', {
clientKey: clientKey ? `${clientKey.substring(0, 8)}...` : 'NOT SET',
redirectUri,
scopes: scopeString,
fullUrl: authUrl,
})
return NextResponse.redirect(authUrl)
} catch (error) {
logger.error('Error initiating TikTok authorization:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -1,130 +0,0 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { getBaseUrl } from '@/lib/core/utils/urls'
const logger = createLogger('TikTokCallback')
export const dynamic = 'force-dynamic'
export async function GET(request: NextRequest) {
const baseUrl = getBaseUrl()
try {
const session = await getSession()
if (!session?.user?.id) {
logger.error('No session found during TikTok callback')
return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`)
}
const searchParams = request.nextUrl.searchParams
const code = searchParams.get('code')
const state = searchParams.get('state')
const error = searchParams.get('error')
const errorDescription = searchParams.get('error_description')
// Handle errors from TikTok
if (error) {
logger.error('TikTok authorization error:', { error, errorDescription })
return NextResponse.redirect(
`${baseUrl}/workspace?error=tiktok_auth_failed&message=${encodeURIComponent(errorDescription || error)}`
)
}
if (!code) {
logger.error('No authorization code received from TikTok')
return NextResponse.redirect(`${baseUrl}/workspace?error=no_code`)
}
// Parse state to get return URL
let returnUrl = `${baseUrl}/workspace`
if (state) {
try {
const stateData = JSON.parse(Buffer.from(state, 'base64url').toString())
returnUrl = stateData.returnUrl || returnUrl
} catch {
logger.warn('Failed to parse state parameter')
}
}
const clientKey = env.TIKTOK_CLIENT_ID
const clientSecret = env.TIKTOK_CLIENT_SECRET
if (!clientKey || !clientSecret) {
logger.error('TikTok credentials not configured')
return NextResponse.redirect(`${baseUrl}/workspace?error=config_error`)
}
const redirectUri = `${baseUrl}/api/auth/tiktok/callback`
// Exchange authorization code for access token
// TikTok uses client_key instead of client_id
const tokenResponse = await fetch('https://open.tiktokapis.com/v2/oauth/token/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_key: clientKey,
client_secret: clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
}).toString(),
})
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text()
logger.error('Failed to exchange code for token:', {
status: tokenResponse.status,
error: errorText,
})
return NextResponse.redirect(`${baseUrl}/workspace?error=token_exchange_failed`)
}
const tokenData = await tokenResponse.json()
if (tokenData.error) {
logger.error('TikTok token error:', tokenData)
return NextResponse.redirect(
`${baseUrl}/workspace?error=tiktok_token_error&message=${encodeURIComponent(tokenData.error_description || tokenData.error)}`
)
}
const { access_token, refresh_token, expires_in, open_id, scope } = tokenData
if (!access_token) {
logger.error('No access token in TikTok response:', tokenData)
return NextResponse.redirect(`${baseUrl}/workspace?error=no_access_token`)
}
// Store the tokens by calling the store endpoint
const storeResponse = await fetch(`${baseUrl}/api/auth/tiktok/store`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: request.headers.get('cookie') || '',
},
body: JSON.stringify({
accessToken: access_token,
refreshToken: refresh_token,
expiresIn: expires_in,
openId: open_id,
scope,
}),
})
if (!storeResponse.ok) {
const storeError = await storeResponse.text()
logger.error('Failed to store TikTok tokens:', storeError)
return NextResponse.redirect(`${baseUrl}/workspace?error=store_failed`)
}
logger.info('TikTok authorization successful')
return NextResponse.redirect(`${returnUrl}?tiktok_connected=true`)
} catch (error) {
logger.error('Error in TikTok callback:', error)
return NextResponse.redirect(`${baseUrl}/workspace?error=callback_error`)
}
}

View File

@@ -1,108 +0,0 @@
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getTikTokRefreshTokenExpiry } from '@/lib/oauth/utils'
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
import { db } from '@/../../packages/db'
import { account } from '@/../../packages/db/schema'
const logger = createLogger('TikTokStore')
export const dynamic = 'force-dynamic'
export async function POST(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn('Unauthorized attempt to store TikTok token')
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { accessToken, refreshToken, expiresIn, openId, scope } = body
if (!accessToken || !openId) {
return NextResponse.json(
{ success: false, error: 'Access token and open_id required' },
{ status: 400 }
)
}
// Fetch user info from TikTok to get display name
let displayName = 'TikTok User'
let avatarUrl: string | undefined
try {
const userResponse = await fetch(
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,union_id,avatar_url,display_name',
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
if (userResponse.ok) {
const userData = await userResponse.json()
if (userData.data?.user) {
displayName = userData.data.user.display_name || displayName
avatarUrl = userData.data.user.avatar_url
}
}
} catch (error) {
logger.warn('Failed to fetch TikTok user info:', error)
}
const existing = await db.query.account.findFirst({
where: and(eq(account.userId, session.user.id), eq(account.providerId, 'tiktok')),
})
const now = new Date()
const accessTokenExpiresAt = expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined
const refreshTokenExpiresAt = getTikTokRefreshTokenExpiry()
if (existing) {
await db
.update(account)
.set({
accessToken,
refreshToken,
accountId: openId,
scope:
scope || 'user.info.basic,user.info.profile,user.info.stats,video.list,video.publish',
accessTokenExpiresAt,
refreshTokenExpiresAt,
updatedAt: now,
})
.where(eq(account.id, existing.id))
logger.info('Updated existing TikTok account', { accountId: openId })
} else {
await safeAccountInsert(
{
id: `tiktok_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'tiktok',
accountId: openId,
accessToken,
refreshToken,
scope:
scope || 'user.info.basic,user.info.profile,user.info.stats,video.list,video.publish',
accessTokenExpiresAt,
refreshTokenExpiresAt,
createdAt: now,
updatedAt: now,
},
{ provider: 'TikTok', identifier: openId }
)
logger.info('Created new TikTok account', { accountId: openId })
}
return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error storing TikTok token:', error)
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -294,13 +294,6 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'user-follow-modify': 'Follow and unfollow artists and users',
'user-read-playback-position': 'View playback position in podcasts',
'ugc-image-upload': 'Upload images to Spotify playlists',
// TikTok scopes
'user.info.basic': 'View basic profile info (avatar, display name)',
'user.info.profile': 'View profile details (bio, verified status)',
'user.info.stats': 'View account statistics (likes, followers, video count)',
'video.list': 'View public videos',
'video.publish': 'Post content to profile',
'video.upload': 'Upload content as draft',
}
function getScopeDescription(scope: string): string {
@@ -380,13 +373,6 @@ export function OAuthRequiredModal({
return
}
if (providerId === 'tiktok') {
onClose()
const returnUrl = encodeURIComponent(window.location.href)
window.location.href = `/api/auth/tiktok/authorize?returnUrl=${returnUrl}`
return
}
await client.oauth2.link({
providerId,
callbackURL: window.location.href,

View File

@@ -1,291 +0,0 @@
import { TikTokIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { TikTokResponse } from '@/tools/tiktok/types'
export const TikTokBlock: BlockConfig<TikTokResponse> = {
type: 'tiktok',
name: 'TikTok',
description: 'Access TikTok user profiles, videos, and publish content',
authMode: AuthMode.OAuth,
longDescription:
'Integrate TikTok into your workflow. Get user profile information including follower counts and video statistics. List and query videos with cover images, embed links, and metadata. Publish videos directly to TikTok from public URLs.',
docsLink: 'https://docs.sim.ai/tools/tiktok',
category: 'tools',
bgColor: '#000000',
icon: TikTokIcon,
subBlocks: [
// Operation selection
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Get User Info', id: 'get_user' },
{ label: 'List Videos', id: 'list_videos' },
{ label: 'Query Videos', id: 'query_videos' },
{ label: 'Query Creator Info', id: 'query_creator_info' },
{ label: 'Direct Post Video', id: 'direct_post_video' },
{ label: 'Get Post Status', id: 'get_post_status' },
],
value: () => 'get_user',
},
// TikTok OAuth Authentication
{
id: 'credential',
title: 'TikTok Account',
type: 'oauth-input',
serviceId: 'tiktok',
placeholder: 'Select TikTok account',
required: true,
},
// Get User Info specific fields
{
id: 'fields',
title: 'Fields',
type: 'short-input',
placeholder: 'open_id,display_name,avatar_url,follower_count,video_count',
condition: {
field: 'operation',
value: 'get_user',
},
},
// List Videos specific fields
{
id: 'maxCount',
title: 'Max Count',
type: 'short-input',
placeholder: '20',
condition: {
field: 'operation',
value: 'list_videos',
},
},
{
id: 'cursor',
title: 'Cursor',
type: 'short-input',
placeholder: 'Pagination cursor from previous response',
condition: {
field: 'operation',
value: 'list_videos',
},
},
// Query Videos specific fields
{
id: 'videoIds',
title: 'Video IDs',
type: 'long-input',
placeholder: 'Comma-separated video IDs (e.g., 7077642457847994444,7080217258529732386)',
condition: {
field: 'operation',
value: 'query_videos',
},
required: {
field: 'operation',
value: 'query_videos',
},
},
// Direct Post Video specific fields
{
id: 'videoUrl',
title: 'Video URL',
type: 'short-input',
placeholder: 'https://example.com/video.mp4',
condition: {
field: 'operation',
value: 'direct_post_video',
},
required: {
field: 'operation',
value: 'direct_post_video',
},
},
{
id: 'title',
title: 'Caption',
type: 'long-input',
placeholder: 'Video caption with #hashtags and @mentions',
condition: {
field: 'operation',
value: 'direct_post_video',
},
},
{
id: 'privacyLevel',
title: 'Privacy Level',
type: 'dropdown',
options: [
{ label: 'Public', id: 'PUBLIC_TO_EVERYONE' },
{ label: 'Friends', id: 'MUTUAL_FOLLOW_FRIENDS' },
{ label: 'Followers', id: 'FOLLOWER_OF_CREATOR' },
{ label: 'Only Me', id: 'SELF_ONLY' },
],
value: () => 'PUBLIC_TO_EVERYONE',
condition: {
field: 'operation',
value: 'direct_post_video',
},
},
{
id: 'disableComment',
title: 'Disable Comments',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: {
field: 'operation',
value: 'direct_post_video',
},
},
// Get Post Status specific fields
{
id: 'publishId',
title: 'Publish ID',
type: 'short-input',
placeholder: 'v_pub_file~v2-1.123456789',
condition: {
field: 'operation',
value: 'get_post_status',
},
required: {
field: 'operation',
value: 'get_post_status',
},
},
],
tools: {
access: [
'tiktok_get_user',
'tiktok_list_videos',
'tiktok_query_videos',
'tiktok_query_creator_info',
'tiktok_direct_post_video',
'tiktok_get_post_status',
],
config: {
tool: (inputs) => {
const operation = inputs.operation || 'get_user'
switch (operation) {
case 'list_videos':
return 'tiktok_list_videos'
case 'query_videos':
return 'tiktok_query_videos'
case 'query_creator_info':
return 'tiktok_query_creator_info'
case 'direct_post_video':
return 'tiktok_direct_post_video'
case 'get_post_status':
return 'tiktok_get_post_status'
default:
return 'tiktok_get_user'
}
},
params: (inputs) => {
const operation = inputs.operation || 'get_user'
const { credential } = inputs
switch (operation) {
case 'get_user':
return {
accessToken: credential,
...(inputs.fields && { fields: inputs.fields }),
}
case 'list_videos':
return {
accessToken: credential,
...(inputs.maxCount && { maxCount: Number(inputs.maxCount) }),
...(inputs.cursor && { cursor: Number(inputs.cursor) }),
}
case 'query_videos':
return {
accessToken: credential,
videoIds: inputs.videoIds
? inputs.videoIds.split(',').map((id: string) => id.trim())
: [],
}
case 'query_creator_info':
return {
accessToken: credential,
}
case 'direct_post_video':
return {
accessToken: credential,
videoUrl: inputs.videoUrl || '',
privacyLevel: inputs.privacyLevel || 'PUBLIC_TO_EVERYONE',
...(inputs.title && { title: inputs.title }),
...(inputs.disableComment === 'true' && { disableComment: true }),
}
case 'get_post_status':
return {
accessToken: credential,
publishId: inputs.publishId || '',
}
default:
return {
accessToken: credential,
}
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'TikTok access token' },
fields: { type: 'string', description: 'Comma-separated list of user fields to return' },
maxCount: { type: 'number', description: 'Maximum number of videos to return (1-20)' },
cursor: { type: 'number', description: 'Pagination cursor from previous response' },
videoIds: { type: 'string', description: 'Comma-separated list of video IDs to query' },
videoUrl: { type: 'string', description: 'Public URL of the video to post' },
title: { type: 'string', description: 'Video caption/description' },
privacyLevel: { type: 'string', description: 'Privacy level for the video' },
disableComment: { type: 'string', description: 'Whether to disable comments' },
publishId: { type: 'string', description: 'Publish ID to check status for' },
},
outputs: {
// Get User outputs
openId: { type: 'string', description: 'TikTok user ID' },
displayName: { type: 'string', description: 'User display name' },
avatarUrl: { type: 'string', description: 'Profile image URL' },
bioDescription: { type: 'string', description: 'User bio' },
followerCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of accounts followed' },
likesCount: { type: 'number', description: 'Total likes received' },
videoCount: { type: 'number', description: 'Total public videos' },
isVerified: { type: 'boolean', description: 'Whether account is verified' },
// List/Query Videos outputs
videos: { type: 'json', description: 'Array of video objects' },
hasMore: { type: 'boolean', description: 'Whether more videos are available' },
// Query Creator Info outputs
creatorAvatarUrl: { type: 'string', description: 'Creator avatar URL' },
creatorUsername: { type: 'string', description: 'Creator username' },
creatorNickname: { type: 'string', description: 'Creator nickname' },
privacyLevelOptions: { type: 'json', description: 'Available privacy levels for posting' },
commentDisabled: { type: 'boolean', description: 'Whether comments are disabled by default' },
duetDisabled: { type: 'boolean', description: 'Whether duets are disabled by default' },
stitchDisabled: { type: 'boolean', description: 'Whether stitches are disabled by default' },
maxVideoPostDurationSec: { type: 'number', description: 'Max video duration in seconds' },
// Direct Post Video outputs
publishId: { type: 'string', description: 'Publish ID for tracking post status' },
// Get Post Status outputs
status: {
type: 'string',
description: 'Post status (PROCESSING_DOWNLOAD, PUBLISH_COMPLETE, FAILED)',
},
failReason: { type: 'string', description: 'Reason for failure if status is FAILED' },
publiclyAvailablePostId: {
type: 'json',
description: 'Array of public post IDs when published',
},
},
}

View File

@@ -131,7 +131,6 @@ import { TavilyBlock } from '@/blocks/blocks/tavily'
import { TelegramBlock } from '@/blocks/blocks/telegram'
import { TextractBlock } from '@/blocks/blocks/textract'
import { ThinkingBlock } from '@/blocks/blocks/thinking'
import { TikTokBlock } from '@/blocks/blocks/tiktok'
import { TinybirdBlock } from '@/blocks/blocks/tinybird'
import { TranslateBlock } from '@/blocks/blocks/translate'
import { TrelloBlock } from '@/blocks/blocks/trello'
@@ -304,7 +303,6 @@ export const registry: Record<string, BlockConfig> = {
supabase: SupabaseBlock,
tavily: TavilyBlock,
telegram: TelegramBlock,
tiktok: TikTokBlock,
textract: TextractBlock,
thinking: ThinkingBlock,
tinybird: TinybirdBlock,

View File

@@ -3472,14 +3472,6 @@ export function HumanInTheLoopIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function TikTokIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'>
<path d='M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z' />
</svg>
)
}
export function TrelloIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -23,8 +23,6 @@ import {
renderPasswordResetEmail,
renderWelcomeEmail,
} from '@/components/emails'
import { createAnonymousSession, ensureAnonymousUserExists } from '@/lib/auth/anonymous'
import { SSO_TRUSTED_PROVIDERS } from '@/lib/auth/sso/constants'
import { sendPlanWelcomeEmail } from '@/lib/billing'
import { authorizeSubscriptionReference } from '@/lib/billing/authorization'
import { handleNewUser } from '@/lib/billing/core/usage'
@@ -61,15 +59,12 @@ import { sendEmail } from '@/lib/messaging/email/mailer'
import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
import { createAnonymousSession, ensureAnonymousUserExists } from './anonymous'
import { SSO_TRUSTED_PROVIDERS } from './sso/constants'
const logger = createLogger('Auth')
import {
getMicrosoftRefreshTokenExpiry,
getTikTokRefreshTokenExpiry,
isMicrosoftProvider,
isTikTokProvider,
} from '@/lib/oauth/utils'
import { getMicrosoftRefreshTokenExpiry, isMicrosoftProvider } from '@/lib/oauth/microsoft'
const validStripeKey = env.STRIPE_SECRET_KEY
@@ -196,9 +191,7 @@ export const auth = betterAuth({
const refreshTokenExpiresAt = isMicrosoftProvider(account.providerId)
? getMicrosoftRefreshTokenExpiry()
: isTikTokProvider(account.providerId)
? getTikTokRefreshTokenExpiry()
: account.refreshTokenExpiresAt
: account.refreshTokenExpiresAt
await db
.update(schema.account)
@@ -323,13 +316,6 @@ export const auth = betterAuth({
.where(eq(schema.account.id, account.id))
}
if (isTikTokProvider(account.providerId)) {
await db
.update(schema.account)
.set({ refreshTokenExpiresAt: getTikTokRefreshTokenExpiry() })
.where(eq(schema.account.id, account.id))
}
// Sync webhooks for credential sets after connecting a new credential
const requestId = crypto.randomUUID().slice(0, 8)
const userMemberships = await db
@@ -2509,11 +2495,6 @@ export const auth = betterAuth({
},
},
// TikTok provider - REMOVED from generic OAuth
// TikTok uses non-standard OAuth (client_key instead of client_id)
// and cannot work with the generic OAuth plugin.
// TikTok OAuth is handled via custom routes at /api/auth/tiktok/*
// WordPress.com provider
{
providerId: 'wordpress',

View File

@@ -244,8 +244,6 @@ export const env = createEnv({
SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID
SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret
CALCOM_CLIENT_ID: z.string().optional(), // Cal.com OAuth client ID
TIKTOK_CLIENT_ID: z.string().optional(), // TikTok OAuth client ID
TIKTOK_CLIENT_SECRET: z.string().optional(), // TikTok OAuth client secret
// E2B Remote Code Execution
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution

View File

@@ -1,3 +1,4 @@
export * from './microsoft'
export * from './oauth'
export * from './types'
export * from './utils'

View File

@@ -0,0 +1,19 @@
export const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90
export const PROACTIVE_REFRESH_THRESHOLD_DAYS = 7
export const MICROSOFT_PROVIDERS = new Set([
'microsoft-excel',
'microsoft-planner',
'microsoft-teams',
'outlook',
'onedrive',
'sharepoint',
])
export function isMicrosoftProvider(providerId: string): boolean {
return MICROSOFT_PROVIDERS.has(providerId)
}
export function getMicrosoftRefreshTokenExpiry(): Date {
return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000)
}

View File

@@ -32,7 +32,6 @@ import {
ShopifyIcon,
SlackIcon,
SpotifyIcon,
TikTokIcon,
TrelloIcon,
VertexIcon,
WealthboxIcon,
@@ -797,27 +796,6 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'spotify',
},
tiktok: {
name: 'TikTok',
icon: TikTokIcon,
services: {
tiktok: {
name: 'TikTok',
description: 'Access TikTok user profiles, videos, and publish content.',
providerId: 'tiktok',
icon: TikTokIcon,
baseProviderIcon: TikTokIcon,
scopes: [
'user.info.basic',
'user.info.profile',
'user.info.stats',
'video.list',
'video.publish',
],
},
},
defaultService: 'tiktok',
},
}
interface ProviderAuthConfig {
@@ -832,11 +810,6 @@ interface ProviderAuthConfig {
* instead of in the request body. Used by Cal.com.
*/
refreshTokenInAuthHeader?: boolean
/**
* Custom parameter name for client ID in request body.
* Defaults to 'client_id'. TikTok uses 'client_key'.
*/
clientIdParamName?: string
}
/**
@@ -1162,20 +1135,6 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
supportsRefreshTokenRotation: false,
}
}
case 'tiktok': {
const { clientId, clientSecret } = getCredentials(
env.TIKTOK_CLIENT_ID,
env.TIKTOK_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://open.tiktokapis.com/v2/oauth/token/',
clientId,
clientSecret,
useBasicAuth: false,
supportsRefreshTokenRotation: true,
clientIdParamName: 'client_key', // TikTok uses client_key instead of client_id
}
}
default:
throw new Error(`Unsupported provider: ${provider}`)
}
@@ -1212,9 +1171,7 @@ function buildAuthRequest(
headers.Authorization = `Basic ${basicAuth}`
} else {
// Use body credentials - include client credentials in request body
// Use custom param name if specified (e.g., TikTok uses 'client_key' instead of 'client_id')
const clientIdParam = config.clientIdParamName || 'client_id'
bodyParams[clientIdParam] = config.clientId
bodyParams.client_id = config.clientId
if (config.clientSecret) {
bodyParams.client_secret = config.clientSecret
}

View File

@@ -42,7 +42,6 @@ export type OAuthProvider =
| 'wordpress'
| 'spotify'
| 'calcom'
| 'tiktok'
export type OAuthService =
| 'google'
@@ -84,7 +83,6 @@ export type OAuthService =
| 'wordpress'
| 'spotify'
| 'calcom'
| 'tiktok'
export interface OAuthProviderConfig {
name: string

View File

@@ -7,49 +7,6 @@ import type {
ScopeEvaluation,
} from './types'
// =============================================================================
// Refresh Token Configuration
// =============================================================================
// Microsoft refresh token configuration (90 days)
const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90
export const PROACTIVE_REFRESH_THRESHOLD_DAYS = 7
const MICROSOFT_PROVIDERS = new Set([
'microsoft-excel',
'microsoft-planner',
'microsoft-teams',
'outlook',
'onedrive',
'sharepoint',
])
export function isMicrosoftProvider(providerId: string): boolean {
return MICROSOFT_PROVIDERS.has(providerId)
}
export function getMicrosoftRefreshTokenExpiry(): Date {
return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000)
}
// TikTok refresh token configuration (365 days)
// TikTok access tokens expire in 24 hours, refresh tokens are valid for 365 days
const TIKTOK_REFRESH_TOKEN_LIFETIME_DAYS = 365
const TIKTOK_PROVIDERS = new Set(['tiktok'])
export function isTikTokProvider(providerId: string): boolean {
return TIKTOK_PROVIDERS.has(providerId)
}
export function getTikTokRefreshTokenExpiry(): Date {
return new Date(Date.now() + TIKTOK_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000)
}
// =============================================================================
// OAuth Service Utilities
// =============================================================================
/**
* Returns a flat list of all available OAuth services with metadata.
* This is safe to use on the server as it doesn't include React components.

View File

@@ -1625,14 +1625,6 @@ import {
} from '@/tools/telegram'
import { textractParserTool } from '@/tools/textract'
import { thinkingTool } from '@/tools/thinking'
import {
tiktokDirectPostVideoTool,
tiktokGetPostStatusTool,
tiktokGetUserTool,
tiktokListVideosTool,
tiktokQueryCreatorInfoTool,
tiktokQueryVideosTool,
} from '@/tools/tiktok'
import { tinybirdEventsTool, tinybirdQueryTool } from '@/tools/tinybird'
import {
trelloAddCommentTool,
@@ -2739,12 +2731,6 @@ export const tools: Record<string, ToolConfig> = {
telegram_send_photo: telegramSendPhotoTool,
telegram_send_video: telegramSendVideoTool,
telegram_send_document: telegramSendDocumentTool,
tiktok_get_user: tiktokGetUserTool,
tiktok_list_videos: tiktokListVideosTool,
tiktok_query_videos: tiktokQueryVideosTool,
tiktok_query_creator_info: tiktokQueryCreatorInfoTool,
tiktok_direct_post_video: tiktokDirectPostVideoTool,
tiktok_get_post_status: tiktokGetPostStatusTool,
clay_populate: clayPopulateTool,
clerk_list_users: clerkListUsersTool,
clerk_get_user: clerkGetUserTool,

View File

@@ -1,156 +0,0 @@
import type {
TikTokDirectPostVideoParams,
TikTokDirectPostVideoResponse,
} from '@/tools/tiktok/types'
import type { ToolConfig } from '@/tools/types'
export const tiktokDirectPostVideoTool: ToolConfig<
TikTokDirectPostVideoParams,
TikTokDirectPostVideoResponse
> = {
id: 'tiktok_direct_post_video',
name: 'TikTok Direct Post Video',
description:
'Publish a video to TikTok from a public URL. TikTok will fetch the video from the provided URL and post it to the authenticated user account. Rate limit: 6 requests per minute per user.',
version: '1.0.0',
oauth: {
required: true,
provider: 'tiktok',
requiredScopes: ['video.publish'],
},
params: {
videoUrl: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Public URL of the video to post. Must be accessible by TikTok servers.',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Video caption/description. Maximum 2200 characters.',
},
privacyLevel: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Privacy level for the video. Options: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. Note: Unaudited apps may be restricted to SELF_ONLY.',
},
disableDuet: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Disable duet for this video. Defaults to false.',
},
disableStitch: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Disable stitch for this video. Defaults to false.',
},
disableComment: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Disable comments for this video. Defaults to false.',
},
videoCoverTimestampMs: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp in milliseconds to use as the video cover image.',
},
isAigc: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Set to true if the video is AI-generated content (AIGC).',
},
},
request: {
url: () => 'https://open.tiktokapis.com/v2/post/publish/video/init/',
method: 'POST',
headers: (params: TikTokDirectPostVideoParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json; charset=UTF-8',
}),
body: (params: TikTokDirectPostVideoParams) => {
const postInfo: Record<string, unknown> = {
privacy_level: params.privacyLevel,
}
if (params.title) {
postInfo.title = params.title
}
if (params.disableDuet !== undefined) {
postInfo.disable_duet = params.disableDuet
}
if (params.disableStitch !== undefined) {
postInfo.disable_stitch = params.disableStitch
}
if (params.disableComment !== undefined) {
postInfo.disable_comment = params.disableComment
}
if (params.videoCoverTimestampMs !== undefined) {
postInfo.video_cover_timestamp_ms = params.videoCoverTimestampMs
}
if (params.isAigc !== undefined) {
postInfo.is_aigc = params.isAigc
}
return {
post_info: postInfo,
source_info: {
source: 'PULL_FROM_URL',
video_url: params.videoUrl,
},
}
},
},
transformResponse: async (response: Response): Promise<TikTokDirectPostVideoResponse> => {
const data = await response.json()
if (data.error?.code !== 'ok' && data.error?.code) {
return {
success: false,
output: {
publishId: '',
},
error: data.error?.message || 'Failed to initiate video post',
}
}
const publishId = data.data?.publish_id
if (!publishId) {
return {
success: false,
output: {
publishId: '',
},
error: 'No publish ID returned',
}
}
return {
success: true,
output: {
publishId: publishId,
},
}
},
outputs: {
publishId: {
type: 'string',
description:
'Unique identifier for tracking the post status. Use this with the Get Post Status tool to check if the video was successfully published.',
},
},
}

View File

@@ -1,97 +0,0 @@
import type { TikTokGetPostStatusParams, TikTokGetPostStatusResponse } from '@/tools/tiktok/types'
import type { ToolConfig } from '@/tools/types'
export const tiktokGetPostStatusTool: ToolConfig<
TikTokGetPostStatusParams,
TikTokGetPostStatusResponse
> = {
id: 'tiktok_get_post_status',
name: 'TikTok Get Post Status',
description:
'Check the status of a video post initiated with Direct Post Video. Use the publishId returned from the post request to track progress.',
version: '1.0.0',
oauth: {
required: true,
provider: 'tiktok',
requiredScopes: ['video.publish'],
},
params: {
publishId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The publish ID returned from the Direct Post Video tool.',
},
},
request: {
url: () => 'https://open.tiktokapis.com/v2/post/publish/status/fetch/',
method: 'POST',
headers: (params: TikTokGetPostStatusParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json; charset=UTF-8',
}),
body: (params: TikTokGetPostStatusParams) => ({
publish_id: params.publishId,
}),
},
transformResponse: async (response: Response): Promise<TikTokGetPostStatusResponse> => {
const data = await response.json()
if (data.error?.code !== 'ok' && data.error?.code) {
return {
success: false,
output: {
status: '',
failReason: null,
publiclyAvailablePostId: [],
},
error: data.error?.message || 'Failed to fetch post status',
}
}
const statusData = data.data
if (!statusData) {
return {
success: false,
output: {
status: '',
failReason: null,
publiclyAvailablePostId: [],
},
error: 'No status data returned',
}
}
return {
success: true,
output: {
status: statusData.status ?? '',
failReason: statusData.fail_reason ?? null,
publiclyAvailablePostId: statusData.publicaly_available_post_id ?? [],
},
}
},
outputs: {
status: {
type: 'string',
description:
'Current status of the post. Values: PROCESSING_DOWNLOAD (TikTok is downloading the video), PUBLISH_COMPLETE (successfully posted), FAILED (check failReason).',
},
failReason: {
type: 'string',
description: 'Reason for failure if status is FAILED. Null otherwise.',
optional: true,
},
publiclyAvailablePostId: {
type: 'array',
description:
'Array of public post IDs once the video is published. Can be used to construct the TikTok video URL.',
},
},
}

View File

@@ -1,185 +0,0 @@
import type { TikTokGetUserParams, TikTokGetUserResponse } from '@/tools/tiktok/types'
import type { ToolConfig } from '@/tools/types'
export const tiktokGetUserTool: ToolConfig<TikTokGetUserParams, TikTokGetUserResponse> = {
id: 'tiktok_get_user',
name: 'TikTok Get User',
description:
'Get the authenticated TikTok user profile information including display name, avatar, bio, follower count, and video statistics.',
version: '1.0.0',
oauth: {
required: true,
provider: 'tiktok',
requiredScopes: ['user.info.basic'],
},
params: {
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
default:
'open_id,union_id,avatar_url,avatar_url_100,avatar_large_url,display_name,bio_description,profile_deep_link,is_verified,username,follower_count,following_count,likes_count,video_count',
description:
'Comma-separated list of fields to return. Available: open_id, union_id, avatar_url, avatar_url_100, avatar_large_url, display_name, bio_description, profile_deep_link, is_verified, username, follower_count, following_count, likes_count, video_count',
},
},
request: {
url: (params: TikTokGetUserParams) => {
const fields =
params.fields ||
'open_id,union_id,avatar_url,avatar_url_100,avatar_large_url,display_name,bio_description,profile_deep_link,is_verified,username,follower_count,following_count,likes_count,video_count'
return `https://open.tiktokapis.com/v2/user/info/?fields=${encodeURIComponent(fields)}`
},
method: 'GET',
headers: (params: TikTokGetUserParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<TikTokGetUserResponse> => {
const data = await response.json()
if (data.error?.code !== 'ok' && data.error?.code) {
return {
success: false,
output: {
openId: '',
unionId: null,
displayName: '',
avatarUrl: null,
avatarUrl100: null,
avatarLargeUrl: null,
bioDescription: null,
profileDeepLink: null,
isVerified: null,
username: null,
followerCount: null,
followingCount: null,
likesCount: null,
videoCount: null,
},
error: data.error?.message || 'Failed to fetch user info',
}
}
const user = data.data?.user
if (!user) {
return {
success: false,
output: {
openId: '',
unionId: null,
displayName: '',
avatarUrl: null,
avatarUrl100: null,
avatarLargeUrl: null,
bioDescription: null,
profileDeepLink: null,
isVerified: null,
username: null,
followerCount: null,
followingCount: null,
likesCount: null,
videoCount: null,
},
error: 'No user data returned',
}
}
return {
success: true,
output: {
openId: user.open_id ?? '',
unionId: user.union_id ?? null,
displayName: user.display_name ?? '',
avatarUrl: user.avatar_url ?? null,
avatarUrl100: user.avatar_url_100 ?? null,
avatarLargeUrl: user.avatar_large_url ?? null,
bioDescription: user.bio_description ?? null,
profileDeepLink: user.profile_deep_link ?? null,
isVerified: user.is_verified ?? null,
username: user.username ?? null,
followerCount: user.follower_count ?? null,
followingCount: user.following_count ?? null,
likesCount: user.likes_count ?? null,
videoCount: user.video_count ?? null,
},
}
},
outputs: {
openId: {
type: 'string',
description: 'Unique TikTok user ID for this application',
},
unionId: {
type: 'string',
description: 'Unique TikTok user ID across all apps from the same developer',
optional: true,
},
displayName: {
type: 'string',
description: 'User display name',
},
avatarUrl: {
type: 'string',
description: 'Profile image URL',
optional: true,
},
avatarUrl100: {
type: 'string',
description: 'Profile image URL (100x100)',
optional: true,
},
avatarLargeUrl: {
type: 'string',
description: 'Profile image URL (large)',
optional: true,
},
bioDescription: {
type: 'string',
description: 'User bio description',
optional: true,
},
profileDeepLink: {
type: 'string',
description: 'Deep link to user TikTok profile',
optional: true,
},
isVerified: {
type: 'boolean',
description: 'Whether the account is verified',
optional: true,
},
username: {
type: 'string',
description: 'TikTok username',
optional: true,
},
followerCount: {
type: 'number',
description: 'Number of followers',
optional: true,
},
followingCount: {
type: 'number',
description: 'Number of accounts the user follows',
optional: true,
},
likesCount: {
type: 'number',
description: 'Total likes received across all videos',
optional: true,
},
videoCount: {
type: 'number',
description: 'Total number of public videos',
optional: true,
},
},
}

View File

@@ -1,13 +0,0 @@
import { tiktokDirectPostVideoTool } from '@/tools/tiktok/direct_post_video'
import { tiktokGetPostStatusTool } from '@/tools/tiktok/get_post_status'
import { tiktokGetUserTool } from '@/tools/tiktok/get_user'
import { tiktokListVideosTool } from '@/tools/tiktok/list_videos'
import { tiktokQueryCreatorInfoTool } from '@/tools/tiktok/query_creator_info'
import { tiktokQueryVideosTool } from '@/tools/tiktok/query_videos'
export { tiktokGetUserTool }
export { tiktokListVideosTool }
export { tiktokQueryVideosTool }
export { tiktokQueryCreatorInfoTool }
export { tiktokDirectPostVideoTool }
export { tiktokGetPostStatusTool }

View File

@@ -1,133 +0,0 @@
import type {
TikTokListVideosParams,
TikTokListVideosResponse,
TikTokVideo,
} from '@/tools/tiktok/types'
import type { ToolConfig } from '@/tools/types'
export const tiktokListVideosTool: ToolConfig<TikTokListVideosParams, TikTokListVideosResponse> = {
id: 'tiktok_list_videos',
name: 'TikTok List Videos',
description:
"Get a list of the authenticated user's TikTok videos with cover images, titles, and metadata. Supports pagination.",
version: '1.0.0',
oauth: {
required: true,
provider: 'tiktok',
requiredScopes: ['video.list'],
},
params: {
maxCount: {
type: 'number',
required: false,
visibility: 'user-or-llm',
default: 20,
description: 'Maximum number of videos to return (1-20)',
},
cursor: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Cursor for pagination (from previous response)',
},
},
request: {
url: () =>
'https://open.tiktokapis.com/v2/video/list/?fields=id,title,cover_image_url,embed_link,duration,create_time,share_url,video_description,width,height',
method: 'POST',
headers: (params: TikTokListVideosParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: TikTokListVideosParams) => ({
max_count: params.maxCount || 20,
...(params.cursor !== undefined && { cursor: params.cursor }),
}),
},
transformResponse: async (response: Response): Promise<TikTokListVideosResponse> => {
const data = await response.json()
if (data.error?.code !== 'ok' && data.error?.code) {
return {
success: false,
output: {
videos: [],
cursor: null,
hasMore: false,
},
error: data.error?.message || 'Failed to fetch videos',
}
}
const videos: TikTokVideo[] = (data.data?.videos ?? []).map(
(video: Record<string, unknown>) => ({
id: video.id ?? '',
title: video.title ?? null,
coverImageUrl: video.cover_image_url ?? null,
embedLink: video.embed_link ?? null,
duration: video.duration ?? null,
createTime: video.create_time ?? null,
shareUrl: video.share_url ?? null,
videoDescription: video.video_description ?? null,
width: video.width ?? null,
height: video.height ?? null,
})
)
return {
success: true,
output: {
videos,
cursor: data.data?.cursor ?? null,
hasMore: data.data?.has_more ?? false,
},
}
},
outputs: {
videos: {
type: 'array',
description: 'List of TikTok videos',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Video ID' },
title: { type: 'string', description: 'Video title', optional: true },
coverImageUrl: {
type: 'string',
description: 'Cover image URL (may expire)',
optional: true,
},
embedLink: { type: 'string', description: 'Embeddable video URL', optional: true },
duration: { type: 'number', description: 'Video duration in seconds', optional: true },
createTime: {
type: 'number',
description: 'Unix timestamp when video was created',
optional: true,
},
shareUrl: { type: 'string', description: 'Shareable video URL', optional: true },
videoDescription: {
type: 'string',
description: 'Video description/caption',
optional: true,
},
width: { type: 'number', description: 'Video width in pixels', optional: true },
height: { type: 'number', description: 'Video height in pixels', optional: true },
},
},
},
cursor: {
type: 'number',
description: 'Cursor for fetching the next page of results',
optional: true,
},
hasMore: {
type: 'boolean',
description: 'Whether there are more videos to fetch',
},
},
}

View File

@@ -1,127 +0,0 @@
import type {
TikTokQueryCreatorInfoParams,
TikTokQueryCreatorInfoResponse,
} from '@/tools/tiktok/types'
import type { ToolConfig } from '@/tools/types'
export const tiktokQueryCreatorInfoTool: ToolConfig<
TikTokQueryCreatorInfoParams,
TikTokQueryCreatorInfoResponse
> = {
id: 'tiktok_query_creator_info',
name: 'TikTok Query Creator Info',
description:
'Check if the authenticated TikTok user can post content and retrieve their available privacy options, interaction settings, and maximum video duration.',
version: '1.0.0',
oauth: {
required: true,
provider: 'tiktok',
requiredScopes: ['video.publish'],
},
params: {},
request: {
url: () => 'https://open.tiktokapis.com/v2/post/publish/creator_info/query/',
method: 'POST',
headers: (params: TikTokQueryCreatorInfoParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<TikTokQueryCreatorInfoResponse> => {
const data = await response.json()
if (data.error?.code !== 'ok' && data.error?.code) {
return {
success: false,
output: {
creatorAvatarUrl: null,
creatorUsername: null,
creatorNickname: null,
privacyLevelOptions: [],
commentDisabled: false,
duetDisabled: false,
stitchDisabled: false,
maxVideoPostDurationSec: null,
},
error: data.error?.message || 'Failed to query creator info',
}
}
const creatorInfo = data.data
if (!creatorInfo) {
return {
success: false,
output: {
creatorAvatarUrl: null,
creatorUsername: null,
creatorNickname: null,
privacyLevelOptions: [],
commentDisabled: false,
duetDisabled: false,
stitchDisabled: false,
maxVideoPostDurationSec: null,
},
error: 'No creator info returned',
}
}
return {
success: true,
output: {
creatorAvatarUrl: creatorInfo.creator_avatar_url ?? null,
creatorUsername: creatorInfo.creator_username ?? null,
creatorNickname: creatorInfo.creator_nickname ?? null,
privacyLevelOptions: creatorInfo.privacy_level_options ?? [],
commentDisabled: creatorInfo.comment_disabled ?? false,
duetDisabled: creatorInfo.duet_disabled ?? false,
stitchDisabled: creatorInfo.stitch_disabled ?? false,
maxVideoPostDurationSec: creatorInfo.max_video_post_duration_sec ?? null,
},
}
},
outputs: {
creatorAvatarUrl: {
type: 'string',
description: 'URL of the creator avatar',
optional: true,
},
creatorUsername: {
type: 'string',
description: 'TikTok username of the creator',
optional: true,
},
creatorNickname: {
type: 'string',
description: 'Display name/nickname of the creator',
optional: true,
},
privacyLevelOptions: {
type: 'array',
description:
'Available privacy levels for posting (e.g., PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY)',
},
commentDisabled: {
type: 'boolean',
description: 'Whether the creator has disabled comments by default',
},
duetDisabled: {
type: 'boolean',
description: 'Whether the creator has disabled duets by default',
},
stitchDisabled: {
type: 'boolean',
description: 'Whether the creator has disabled stitches by default',
},
maxVideoPostDurationSec: {
type: 'number',
description: 'Maximum allowed video duration in seconds',
optional: true,
},
},
}

View File

@@ -1,119 +0,0 @@
import type {
TikTokQueryVideosParams,
TikTokQueryVideosResponse,
TikTokVideo,
} from '@/tools/tiktok/types'
import type { ToolConfig } from '@/tools/types'
export const tiktokQueryVideosTool: ToolConfig<TikTokQueryVideosParams, TikTokQueryVideosResponse> =
{
id: 'tiktok_query_videos',
name: 'TikTok Query Videos',
description:
'Query specific TikTok videos by their IDs to get fresh metadata including cover images, embed links, and video details.',
version: '1.0.0',
oauth: {
required: true,
provider: 'tiktok',
requiredScopes: ['video.list'],
},
params: {
videoIds: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description: 'Array of video IDs to query (maximum 20)',
items: {
type: 'string',
description: 'TikTok video ID',
},
},
},
request: {
url: () =>
'https://open.tiktokapis.com/v2/video/query/?fields=id,title,cover_image_url,embed_link,duration,create_time,share_url,video_description,width,height',
method: 'POST',
headers: (params: TikTokQueryVideosParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: TikTokQueryVideosParams) => ({
filters: {
video_ids: params.videoIds,
},
}),
},
transformResponse: async (response: Response): Promise<TikTokQueryVideosResponse> => {
const data = await response.json()
if (data.error?.code !== 'ok' && data.error?.code) {
return {
success: false,
output: {
videos: [],
},
error: data.error?.message || 'Failed to query videos',
}
}
const videos: TikTokVideo[] = (data.data?.videos ?? []).map(
(video: Record<string, unknown>) => ({
id: video.id ?? '',
title: video.title ?? null,
coverImageUrl: video.cover_image_url ?? null,
embedLink: video.embed_link ?? null,
duration: video.duration ?? null,
createTime: video.create_time ?? null,
shareUrl: video.share_url ?? null,
videoDescription: video.video_description ?? null,
width: video.width ?? null,
height: video.height ?? null,
})
)
return {
success: true,
output: {
videos,
},
}
},
outputs: {
videos: {
type: 'array',
description: 'List of queried TikTok videos',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Video ID' },
title: { type: 'string', description: 'Video title', optional: true },
coverImageUrl: {
type: 'string',
description: 'Cover image URL (fresh URL)',
optional: true,
},
embedLink: { type: 'string', description: 'Embeddable video URL', optional: true },
duration: { type: 'number', description: 'Video duration in seconds', optional: true },
createTime: {
type: 'number',
description: 'Unix timestamp when video was created',
optional: true,
},
shareUrl: { type: 'string', description: 'Shareable video URL', optional: true },
videoDescription: {
type: 'string',
description: 'Video description/caption',
optional: true,
},
width: { type: 'number', description: 'Video width in pixels', optional: true },
height: { type: 'number', description: 'Video height in pixels', optional: true },
},
},
},
},
}

View File

@@ -1,140 +0,0 @@
import type { ToolResponse } from '@/tools/types'
/**
* Base params that include OAuth access token
*/
export interface TikTokBaseParams {
accessToken: string
}
/**
* Get User Info
*/
export interface TikTokGetUserParams extends TikTokBaseParams {
fields?: string
}
export interface TikTokGetUserResponse extends ToolResponse {
output: {
openId: string
unionId: string | null
displayName: string
avatarUrl: string | null
avatarUrl100: string | null
avatarLargeUrl: string | null
bioDescription: string | null
profileDeepLink: string | null
isVerified: boolean | null
username: string | null
followerCount: number | null
followingCount: number | null
likesCount: number | null
videoCount: number | null
}
}
/**
* List Videos
*/
export interface TikTokListVideosParams extends TikTokBaseParams {
maxCount?: number
cursor?: number
}
export interface TikTokVideo {
id: string
title: string | null
coverImageUrl: string | null
embedLink: string | null
duration: number | null
createTime: number | null
shareUrl: string | null
videoDescription: string | null
width: number | null
height: number | null
}
export interface TikTokListVideosResponse extends ToolResponse {
output: {
videos: TikTokVideo[]
cursor: number | null
hasMore: boolean
}
}
/**
* Query Videos
*/
export interface TikTokQueryVideosParams extends TikTokBaseParams {
videoIds: string[]
}
export interface TikTokQueryVideosResponse extends ToolResponse {
output: {
videos: TikTokVideo[]
}
}
/**
* Query Creator Info - Check posting permissions and get privacy options
*/
export interface TikTokQueryCreatorInfoParams extends TikTokBaseParams {}
export interface TikTokQueryCreatorInfoResponse extends ToolResponse {
output: {
creatorAvatarUrl: string | null
creatorUsername: string | null
creatorNickname: string | null
privacyLevelOptions: string[]
commentDisabled: boolean
duetDisabled: boolean
stitchDisabled: boolean
maxVideoPostDurationSec: number | null
}
}
/**
* Direct Post Video - Publish video from URL to TikTok
*/
export interface TikTokDirectPostVideoParams extends TikTokBaseParams {
videoUrl: string
title?: string
privacyLevel: string
disableDuet?: boolean
disableStitch?: boolean
disableComment?: boolean
videoCoverTimestampMs?: number
isAigc?: boolean
}
export interface TikTokDirectPostVideoResponse extends ToolResponse {
output: {
publishId: string
}
}
/**
* Get Post Status - Check status of a published post
*/
export interface TikTokGetPostStatusParams extends TikTokBaseParams {
publishId: string
}
export interface TikTokGetPostStatusResponse extends ToolResponse {
output: {
status: string
failReason: string | null
publiclyAvailablePostId: string[]
}
}
/**
* Union type of all TikTok responses
*/
export type TikTokResponse =
| TikTokGetUserResponse
| TikTokListVideosResponse
| TikTokQueryVideosResponse
| TikTokQueryCreatorInfoResponse
| TikTokDirectPostVideoResponse
| TikTokGetPostStatusResponse