Compare commits

..

83 Commits

Author SHA1 Message Date
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
Waleed
656a6b8abd fix(sanitization): added more input sanitization to tool routes (#2475)
* fix(sanitization): added more input sanitization to tool routes

* ack PR comments
2025-12-19 01:27:20 -08:00
Waleed
889b44c90a improvement(db): added missing indexes for common access patterns (#2473) 2025-12-19 00:46:10 -08:00
Waleed
3a33ec929f fix(authentication): added auth checks for various routes, mysql and postgres query validation, csp improvements (#2472) 2025-12-19 00:44:52 -08:00
Waleed
24356d99ec fix(unsubscribe): add one-click unsubscribe (#2467)
* fix(unsubscribe): add one-click unsubscribe

* ack Pr comments
2025-12-18 21:16:24 -08:00
Waleed
6de1c04517 feat(i18n): update translations (#2470) 2025-12-18 21:01:51 -08:00
Adam Gough
38be2b76c4 fix(slack): respect message limit, remove duplicate canonical representations (#2469)
* fix(slack): respect message limit, remove duplicate canonical representations

* removed comment

* updated docs script

---------

Co-authored-by: aadamgough <adam@sim.ai>
2025-12-18 20:37:14 -08:00
Waleed
a2f14cab54 feat(og): add opengraph images for templates, blogs, and updated existing opengraph image for all other pages (#2466)
* feat(og): add opengraph images for templates, blogs, and updated existing opengraph image for all other pages

* added to workspace templates page as well

* ack PR comments
2025-12-18 19:15:06 -08:00
Priyanshu Solanki
474762d6fb improvement(hitl): show resume url in tag dropdown within hitl block (#2464)
* fixed the human in the loop url resolution:

* greptilecomments

* greptilecomments

---------

Co-authored-by: Pbonmars-20031006@users.noreply.github.com
2025-12-18 19:43:37 -07:00
Waleed
0005c3e465 feat(i18n): update translations (#2463)
Co-authored-by: aadamgough <aadamgough@users.noreply.github.com>
2025-12-18 18:18:31 -08:00
Adam Gough
fc40b4f7af fix(tools): improved slack output ux and jira params (#2462)
* fixed slack output

* updated jira

* removed comment

* change team uuid
2025-12-18 17:56:10 -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
Priyanshu Solanki
2a7f51a2f6 adding clamps for subflow drag and drops of blocks (#2460)
Co-authored-by: priyanshu.solanki <priyanshu.solanki@saviynt.com>
2025-12-18 16:57:58 -07:00
Waleed
90c3c43607 fix(blog): add back unoptimized tag, fix styling (#2461) 2025-12-18 15:55:47 -08:00
Siddharth Ganesan
83d813a7cc improvement(copilot): add edge handle validation to copilot edit workflow (#2448)
* Add edge handle validation

* Clean

* Fix lint

* Fix empty target handle
2025-12-18 15:40:00 -08:00
Vikhyath Mondreti
811c736705 fix failing lint from os contributor (#2459) 2025-12-18 15:03:31 -08:00
Vikhyath Mondreti
c6757311af Merge branch 'main' into staging 2025-12-18 14:58:48 -08:00
div
b5b12ba2d1 fix(teams): webhook notifications crash (#2426)
* fix(docs): clarify working directory for drizzle migration (#2375)

* fix(landing): prevent url encoding for spaces for footer links (#2376)

* fix: handle empty body.value in Teams webhook notification parser (#2425)

* Update directory path for migration command

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
Co-authored-by: icecrasher321 <icecrasher321@users.noreply.github.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: mosa <mosaxiv@gmail.com>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com>
Co-authored-by: Shivam <shivamprajapati035@gmail.com>
Co-authored-by: Gaurav Chadha <65453826+Chadha93@users.noreply.github.com>
Co-authored-by: root <root@Delta.localdomain>
2025-12-18 14:57:27 -08:00
Waleed
0d30676e34 fix(blog): revert back to using next image tags in blog (#2458) 2025-12-18 13:51:58 -08:00
Waleed
36bdccb449 fix(ui): fixed visibility issue on reset passowrd page (#2456) 2025-12-18 13:24:32 -08:00
Waleed
f45730a89e improvement(helm): added SSO and cloud storage variables to helm charts (#2454)
* improvement(helm): added SSO and cloud storage variables to helm charts

* consolidated sf types
2025-12-18 13:12:21 -08:00
Vikhyath Mondreti
04cd837e9c fix(notifs): inactivity polling filters, consolidate trigger types, minor consistency issue with filter parsing (#2452)
* fix(notifs-slac): display name for account

* fix inactivity polling check

* consolidate trigger types

* remove redundant defaults

* fix
2025-12-18 12:49:58 -08:00
Waleed
c23130a26e Revert "fix(salesforce): updated to more flexible oauth that allows production, developer, and custom domain salesforce orgs (#2441) (#2444)" (#2453)
This reverts commit 9da19e84b7.
2025-12-18 12:46:24 -08:00
Priyanshu Solanki
7575cd6f27 Merge pull request #2451 from simstudioai/improvement/SIM-514-useWebhookUrl-conditioning
improvement(useWebhookUrl): GET api/webhook is called when useWebhookUrl:true
2025-12-18 13:31:06 -07:00
priyanshu.solanki
fbde64f0b0 fixing lint errors 2025-12-18 13:04:25 -07:00
Waleed
25f7ed20f6 feat(docs): added 404 page for the docs (#2450)
* feat(docs): added 404 page for the docs

* added metadata
2025-12-18 11:46:42 -08:00
priyanshu.solanki
261aa3d72d fixing a react component: 2025-12-18 12:39:47 -07:00
Waleed
9da19e84b7 fix(salesforce): updated to more flexible oauth that allows production, developer, and custom domain salesforce orgs (#2441) (#2444)
* fix(oauth): updated oauth providers that had unstable reference IDs leading to duplicate oauth records (#2441)

* fix(oauth): updated oauth providers that had unstable reference IDs leading to duplicate oauth records

* ack PR comments

* ack PR comments

* cleanup salesforce refresh logic

* ack more PR comments
2025-12-18 11:39:28 -08:00
priyanshu.solanki
e83afc0a62 fixing the useWbehookManangement call to only call the loadwebhookorgenerateurl function when the useWebhookurl flag is true 2025-12-18 12:31:18 -07:00
Vikhyath Mondreti
1720fa8749 feat(compare-schema): ci check to make sure schema.ts never goes out of sync with migrations (#2449)
* feat(compare-schema): ci check to make sure schema.ts never goes out of sync with migrations

* test out of sync [do not merge]

* Revert "test out of sync [do not merge]"

This reverts commit 9771f66b84.
2025-12-18 11:25:19 -08:00
Waleed
f3ad7750af fix(auth): added same-origin validation to forget password route, added confirmation for disable auth FF (#2447)
* fix(auth): added same-origin validation to forget password route, added confirmation for disable auth FF

* ack PR comments
2025-12-18 11:07:25 -08:00
Vikhyath Mondreti
78b7643e65 fix(condition): async execution isolated vm error (#2446)
* fix(condition): async execution isolated vm error

* fix tests
2025-12-18 11:02:01 -08:00
Siddharth Ganesan
7ef1150383 fix(workflow-state, copilot): prevent copilot from setting undefined state, fix order of operations for copilot edit workflow, add sleep tool (#2440)
* Fix copilot ooo

* Add copilot sleep tool

* Fix lint
2025-12-18 09:57:01 -08:00
Waleed
67cfb21d08 v0.5.34: servicenow, code cleanup, prevent cyclic edge connections, custom tool fixes 2025-12-17 23:39:10 -08:00
Waleed
a337af92bc fix(custom-tools): added missing _toolSchema to internal param set for agents calling custom tools (#2445) 2025-12-17 23:38:36 -08:00
Waleed
b4a99779eb feat(i18n): update translations (#2443)
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-12-17 20:55:06 -08:00
Waleed
471cb4747c fix(oauth): updated oauth providers that had unstable reference IDs leading to duplicate oauth records (#2441)
* fix(oauth): updated oauth providers that had unstable reference IDs leading to duplicate oauth records

* ack PR comments
2025-12-17 20:45:38 -08:00
Priyanshu Solanki
491bd783b5 fix(servicenow): update servicenow block to use basic auth instead of oauth (#2435)
* fix adding client ID and secret fields to supprot ouath

* revert servicenow to use basic auth instead of oauth

* fix failing tests

---------

Co-authored-by: priyanshu.solanki <priyanshu.solanki@saviynt.com>
Co-authored-by: waleed <walif6@gmail.com>
2025-12-17 20:41:46 -08:00
Waleed
5516fa39c3 fix(graph): prevent cyclic dependencies in graph following ReactFlow examples (#2439)
* fix(graph): prevent cyclic dependencies in graph following ReactFlow examples

* ack PR comment
2025-12-17 19:13:39 -08:00
Waleed
21fa92bc41 feat(i18n): update translations (#2438)
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-12-17 18:01:43 -08:00
Vikhyath Mondreti
26ca37328a fix(conditions): make outputs correct (#2437) 2025-12-17 17:15:16 -08:00
Waleed
731997f768 fix(envvars): cleanup unused envvars (#2436)
* fix(envvars): cleanup unused envvars

* removed unused react-google-drive-picker dep
2025-12-17 17:13:01 -08:00
Waleed
1d6975db49 v0.5.33: loops, chat fixes, subflow resizing refactor, terminal updates 2025-12-17 15:45:39 -08:00
Waleed
c4a6d11cc0 fix(condition): used isolated vms for condition block RCE (#2432)
* fix(condition): used isolated vms for condition block RCE

* ack PR comment

* one more

* remove inputForm from sched, update loop condition to also use isolated vm

* hide servicenow
2025-12-17 15:29:25 -08:00
Waleed
7b5405e968 feat(vertex): added vertex to list of supported providers (#2430)
* feat(vertex): added vertex to list of supported providers

* added utils files for each provider, consolidated gemini utils, added dynamic verbosity and reasoning fetcher
2025-12-17 14:57:58 -08:00
Vikhyath Mondreti
1ae3b47f5c fix(inactivity-poll): need to respect level and trigger filters (#2431) 2025-12-17 14:50:33 -08:00
Waleed
3120a785df fix(terminal): fix text wrap for errors and messages with long strings (#2429) 2025-12-17 13:42:43 -08:00
Vikhyath Mondreti
8775e76c32 improvement(subflow): resize vertical height estimate (#2428)
* improvement(node-dims): share constants for node padding

* fix vertical height estimation
2025-12-17 12:07:57 -08:00
Vikhyath Mondreti
9a6c68789d fix(subflow): resizing live update 2025-12-17 11:49:24 -08:00
Waleed
08bc1125bd fix(cmd-k): when navigating to current workspace/workflow, close modal instead of navigating (#2420)
* fix(cmd-k): when navigating to current workspace, close modal instead of navigating

* ack PR comment
2025-12-17 10:21:35 -08:00
Waleed
f4f74da1dc feat(i18n): update translations (#2421)
Co-authored-by: icecrasher321 <icecrasher321@users.noreply.github.com>
2025-12-17 10:21:15 -08:00
Vikhyath Mondreti
de330d80f5 improvement(mcp): restructure mcp tools caching/fetching info to improve UX (#2416)
* feat(mcp): improve cache practice

* restructure mcps fetching, caching, UX indicators

* fix schema

* styling improvements

* fix tooltips and render issue

* fix loading sequence + add redis

---------

Co-authored-by: waleed <walif6@gmail.com>
2025-12-16 21:23:18 -08:00
Emir Karabeg
b7228d57f7 feat(service-now): added service now block (#2404)
* feat(service-now): added service now block

* fix: bun lock

* improvement: fixed @trigger.dev/sdk imports and removal of sentry blocks

* improvement: fixed @trigger.dev/sdk import

* improvement: fixed @trigger.dev/sdk import

* fix(servicenow): save accessTokenExpiresAt on initial OAuth account creation

* docs(servicenow): add ServiceNow tool documentation and icon mapping

* fixing bun lint issues

* fixing username/password fields

* fixing test file for refreshaccesstoken to support instance uri

* removing basic auth and fixing undo-redo/store.ts

* removed import set api code, changed CRUD operations to CRUD_record and added wand configuration to help users to generate JSON Arrays

---------

Co-authored-by: priyanshu.solanki <priyanshu.solanki@saviynt.com>
2025-12-16 21:16:09 -08:00
Waleed
837aabca5e v0.5.32: google sheets fix, schedule input format 2025-12-16 15:41:04 -08:00
Vikhyath Mondreti
f9cfca92bf v0.5.31: add zod as direct dep 2025-12-15 20:40:02 -08:00
Waleed
25afacb25e v0.5.30: vllm fixes, permissions fixes, isolated vms for code execution, tool fixes 2025-12-15 19:38:01 -08:00
Gaurav Chadha
fcf52ac4d5 fix(landing): prevent url encoding for spaces for footer links (#2376) 2025-12-15 10:59:12 -08:00
Shivam
842200bcf2 fix(docs): clarify working directory for drizzle migration (#2375) 2025-12-15 10:58:27 -08:00
Waleed
a0fb889644 v0.5.29: chat voice mode, opengraph for docs, option to disable auth 2025-12-13 19:50:06 -08:00
Waleed
f526c36fc0 v0.5.28: tool fixes, sqs, spotify, nextjs update, component playground 2025-12-12 21:05:57 -08:00
Waleed
e24f31cbce v0.5.27: sidebar updates, ssrf patches, gpt-5.2, stagehand fixes 2025-12-11 14:45:25 -08:00
Waleed
3fbd57caf1 v0.5.26: tool fixes, templates and knowledgebase fixes, deployment versions in logs 2025-12-11 00:52:13 -08:00
Vikhyath Mondreti
b5da61377c v0.5.25: minor ui improvements, copilot billing fix 2025-12-10 18:32:27 -08:00
Waleed
18b7032494 v0.5.24: agent tool and UX improvements, redis service overhaul (#2291)
* feat(folders): add the ability to create a folder within a folder in popover (#2287)

* fix(agent): filter out empty params to ensure LLM can set tool params at runtime (#2288)

* fix(mcp): added backfill effect to add missing descriptions for mcp tools (#2290)

* fix(redis): cleanup access pattern across callsites (#2289)

* fix(redis): cleanup access pattern across callsites

* swap redis command to be non blocking

* improvement(log-details): polling, trace spans (#2292)

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
2025-12-10 13:09:21 -08:00
Waleed
b7bbef8620 v0.5.23: kb, logs, general ui improvements, token bucket rate limits, docs, mcp, autolayout improvements (#2286)
* fix(mcp): prevent redundant MCP server discovery calls at runtime, use cached tool schema instead (#2273)

* fix(mcp): prevent redundant MCP server discovery calls at runtime, use cached tool schema instead

* added backfill, added loading state for tools in settings > mcp

* fix tool inp

* feat(rate-limiter): token bucket algorithm  (#2270)

* fix(ratelimit): make deployed chat rate limited

* improvement(rate-limiter): use token bucket algo

* update docs

* fix

* fix type

* fix db rate limiter

* address greptile comments

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

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

* fix(tools): updated kalshi and polymarket tools to accurately reflect outputs (#2274)

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

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

* fix(autolayout): align by handle (#2277)

* fix(autolayout): align by handle

* use shared constants everywhere

* cleanup

* fix(copilot): fix custom tools (#2278)

* Fix title custom tool

* Checkpoitn (broken)

* Fix custom tool flash

* Edit workflow returns null fix

* Works

* Fix lint

* fix(ime): prevent form submission during IME composition steps (#2279)

* fix(ui): prevent form submission during IME composition steps

* chore(gitignore): add IntelliJ IDE files to .gitignore

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* feat(ui): logs, kb, emcn (#2207)

* feat(kb): emcn alignment; sidebar: popover primary; settings-modal: expand

* feat: EMCN breadcrumb; improvement(KB): UI

* fix: hydration error

* improvement(KB): UI

* feat: emcn modal sizing, KB tags; refactor: deleted old sidebar

* feat(logs): UI

* fix: add documents modal name

* feat: logs, emcn, cursorrules; refactor: logs

* feat: dashboard

* feat: notifications; improvement: logs details

* fixed random rectangle on canvas

* fixed the name of the file to align

* fix build

---------

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

* fix(creds): glitch allowing multiple credentials in an integration (#2282)

* improvement: custom tools modal, logs-details (#2283)

* fix(docs): fix copy page button and header hook (#2284)

* improvement(chat): add the ability to download files from the deployed chat (#2280)

* added teams download and chat download file

* Removed comments

* removed comments

* component structure and download all

* removed comments

* cleanup code

* fix empty files case

* small fix

* fix(container): resize heuristic improvement (#2285)

* estimate block height for resize based on subblocks

* fix hydration error

* make more conservative

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: icecrasher321 <icecrasher321@users.noreply.github.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: mosa <mosaxiv@gmail.com>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com>
2025-12-10 00:57:58 -08:00
Waleed
52edbea659 v0.5.22: rss feed trigger, sftp tool, billing fixes, 413 surfacing, copilot improvements 2025-12-09 10:27:36 -08:00
Vikhyath Mondreti
d480057fd3 fix(migration): migration got removed by force push (#2253) 2025-12-08 14:08:12 -08:00
Waleed
c27c233da0 v0.5.21: google groups, virtualized code viewer, ui, autolayout, docs improvements 2025-12-08 13:10:50 -08:00
Waleed
ebef5f3a27 v0.5.20: google slides, ui fixes, subflow resizing improvements 2025-12-06 15:36:09 -08:00
Vikhyath Mondreti
12c4c2d44f v0.5.19: copilot fix 2025-12-05 15:27:31 -08:00
Vikhyath Mondreti
929a352edb fix(build): added trigger.dev sdk mock to tests (#2216) 2025-12-05 14:26:50 -08:00
Vikhyath Mondreti
6cd078b0fe v0.5.18: ui fixes, nextjs16, workspace notifications, admin APIs, loading improvements, new slack tools 2025-12-05 14:03:09 -08:00
Waleed
31874939ee v0.5.17: modals, billing fixes, bun update, zoom, dropbox, kalshi, polymarket, datadog, ahrefs, gitlab, shopify, ssh, wordpress integrations 2025-12-04 13:29:46 -08:00
Waleed
e157ce5fbc v0.5.16: MCP fixes, code refactors, jira fixes, new mistral models 2025-12-02 22:02:11 -08:00
Vikhyath Mondreti
774e5d585c v0.5.15: add tools, revert subblock prop change 2025-12-01 13:52:12 -08:00
Vikhyath Mondreti
54cc93743f v0.5.14: fix issue with teams, google selectors + cleanup code 2025-12-01 12:39:39 -08:00
Waleed
8c32ad4c0d v0.5.13: polling fixes, generic agent search tool, status page, smtp, sendgrid, linkedin, more tools (#2148)
* feat(tools): added smtp, sendgrid, mailgun, linkedin, fixed permissions in context menu (#2133)

* feat(tools): added twilio sendgrid integration

* feat(tools): added smtp, sendgrid, mailgun, fixed permissions in context menu

* added top level mocks for sporadically failing tests

* incr type safety

* fix(team-plans): track departed member usage so value not lost (#2118)

* fix(team-plans): track departed member usage so value not lost

* reset usage to 0 when they leave team

* prep merge with stagig

* regen migrations

* fix org invite + ws selection'

---------

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

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

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

* feat(creators): add verification for creators (#2135)

* feat(tools): added apify block/tools  (#2136)

* feat(tools): added apify

* cleanup

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

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

* feat(env): added more optional env var examples (#2138)

* feat(statuspage): added statuspage, updated list of tools in footer, renamed routes (#2139)

* feat(statuspage): added statuspage, updated list of tools in footer, renamed routes

* ack PR comments

* feat(tools): add generic search tool (#2140)

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

* fix(sdks): bump sdk versions (#2142)

* fix(webhooks): count test webhooks towards usage limit (#2143)

* fix(bill): add requestId to webhook processing (#2144)

* improvement(subflow): remove all associated edges when moving a block into a subflow (#2145)

* improvement(subflow): remove all associated edges when moving a block into a subflow

* ack PR comments

* fix(polling): mark webhook failed on webhook trigger errors (#2146)

* fix(deps): declare core transient deps explicitly (#2147)

* fix(deps): declare core transient deps explicitly

* ack PR comments

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-12-01 10:15:36 -08:00
Waleed
1d08796853 v0.5.12: memory optimizations, sentry, incidentio, posthog, zendesk, pylon, intercom, mailchimp, loading optimizations (#2132)
* fix(memory-util): fixed unbounded array of gmail/outlook pollers causing high memory util, added missing db indexes/removed unused ones, auto-disable schedules/webhooks after 10 consecutive failures (#2115)

* fix(memory-util): fixed unbounded array of gmail/outlook pollers causing high memory util, added missing db indexes/removed unused ones, auto-disable schedules/webhooks after 10 consecutive failures

* ack PR comments

* ack

* improvement(teams-plan): seats increase simplification + not triggering checkout session (#2117)

* improvement(teams-plan): seats increase simplification + not triggering checkout session

* cleanup via helper

* feat(tools): added sentry, incidentio, and posthog tools (#2116)

* feat(tools): added sentry, incidentio, and posthog tools

* update docs

* fixed docs to use native fumadocs for llms.txt and copy markdown, fixed tool issues

* cleanup

* enhance error extractor, fixed posthog tools

* docs enhancements, cleanup

* added more incident io ops, remove zustand/shallow in favor of zustand/react/shallow

* fix type errors

* remove unnecessary comments

* added vllm to docs

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

* feat(i18n): update translations

* fix build

---------

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

* improvement(workflow-execution): perf improvements to passing workflow state + decrypted env vars (#2119)

* improvement(execution): load workflow state once instead of 2-3 times

* decrypt only in get helper

* remove comments

* remove comments

* feat(models): host google gemini models (#2122)

* feat(models): host google gemini models

* remove unused primary key

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

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

* feat(tools): added zendesk, pylon, intercom, & mailchimp (#2126)

* feat(tools): added zendesk, pylon, intercom, & mailchimp

* finish zendesk and pylon

* updated docs

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

* feat(i18n): update translations

* fixed build

---------

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

* fix(permissions): add client-side permissions validation to prevent unauthorized actions, upgraded custom tool modal (#2130)

* fix(permissions): add client-side permissions validation to prevent unauthorized actions, upgraded custom tool modal

* fix failing test

* fix test

* cleanup

* fix(custom-tools): add composite index on custom tool names & workspace id (#2131)

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-11-28 16:08:06 -08:00
Waleed
ebcd243942 v0.5.11: stt, videogen, vllm, billing fixes, new models 2025-11-25 01:14:12 -08:00
Waleed
b7e814b721 v0.5.10: copilot upgrade, preprocessor, logs search, UI, code hygiene 2025-11-21 12:04:34 -08:00
Waleed
842ef27ed9 v0.5.9: add backwards compatibility for agent messages array 2025-11-20 11:19:42 -08:00
Vikhyath Mondreti
31c34b2ea3 v0.5.8: notifications, billing, ui changes, store loading state machine 2025-11-20 01:32:32 -08:00
Vikhyath Mondreti
8f0ef58056 v0.5.7: combobox selectors, usage indicator, workflow loading race condition, other improvements 2025-11-17 21:25:51 -08:00
310 changed files with 34547 additions and 4141 deletions

View File

@@ -48,6 +48,19 @@ jobs:
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
run: bun run test
- name: Check schema and migrations are in sync
working-directory: packages/db
run: |
bunx drizzle-kit generate --config=./drizzle.config.ts
if [ -n "$(git status --porcelain ./migrations)" ]; then
echo "❌ Schema and migrations are out of sync!"
echo "Run 'cd packages/db && bunx drizzle-kit generate' and commit the new migrations."
git status --porcelain ./migrations
git diff ./migrations
exit 1
fi
echo "✅ Schema and migrations are in sync"
- name: Build application
env:
NODE_OPTIONS: '--no-warnings'

View File

@@ -188,6 +188,7 @@ DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
Then run the migrations:
```bash
cd packages/db # Required so drizzle picks correct .env file
bunx drizzle-kit migrate --config=./drizzle.config.ts
```

View File

@@ -0,0 +1,23 @@
import { DocsBody, DocsPage } from 'fumadocs-ui/page'
export const metadata = {
title: 'Page Not Found',
}
export default function NotFound() {
return (
<DocsPage>
<DocsBody>
<div className='flex min-h-[60vh] flex-col items-center justify-center text-center'>
<h1 className='mb-4 bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] bg-clip-text font-bold text-8xl text-transparent'>
404
</h1>
<h2 className='mb-2 font-semibold text-2xl text-foreground'>Page Not Found</h2>
<p className='text-muted-foreground'>
The page you're looking for doesn't exist or has been moved.
</p>
</div>
</DocsBody>
</DocsPage>
)
}

View File

@@ -6,7 +6,10 @@ import { source } from '@/lib/source'
export const revalidate = false
export async function GET(_req: NextRequest, { params }: { params: Promise<{ slug?: string[] }> }) {
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> }
) {
const { slug } = await params
let lang: (typeof i18n.languages)[number] = i18n.defaultLanguage

View File

@@ -2452,6 +2452,56 @@ export const GeminiIcon = (props: SVGProps<SVGSVGElement>) => (
</svg>
)
export const VertexIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
{...props}
id='standard_product_icon'
xmlns='http://www.w3.org/2000/svg'
version='1.1'
viewBox='0 0 512 512'
>
<g id='bounding_box'>
<rect width='512' height='512' fill='none' />
</g>
<g id='art'>
<path
d='M128,244.99c-8.84,0-16-7.16-16-16v-95.97c0-8.84,7.16-16,16-16s16,7.16,16,16v95.97c0,8.84-7.16,16-16,16Z'
fill='#ea4335'
/>
<path
d='M256,458c-2.98,0-5.97-.83-8.59-2.5l-186-122c-7.46-4.74-9.65-14.63-4.91-22.09,4.75-7.46,14.64-9.65,22.09-4.91l177.41,116.53,177.41-116.53c7.45-4.74,17.34-2.55,22.09,4.91,4.74,7.46,2.55,17.34-4.91,22.09l-186,122c-2.62,1.67-5.61,2.5-8.59,2.5Z'
fill='#fbbc04'
/>
<path
d='M256,388.03c-8.84,0-16-7.16-16-16v-73.06c0-8.84,7.16-16,16-16s16,7.16,16,16v73.06c0,8.84-7.16,16-16,16Z'
fill='#34a853'
/>
<circle cx='128' cy='70' r='16' fill='#ea4335' />
<circle cx='128' cy='292' r='16' fill='#ea4335' />
<path
d='M384.23,308.01c-8.82,0-15.98-7.14-16-15.97l-.23-94.01c-.02-8.84,7.13-16.02,15.97-16.03h.04c8.82,0,15.98,7.14,16,15.97l.23,94.01c.02,8.84-7.13,16.02-15.97,16.03h-.04Z'
fill='#4285f4'
/>
<circle cx='384' cy='70' r='16' fill='#4285f4' />
<circle cx='384' cy='134' r='16' fill='#4285f4' />
<path
d='M320,220.36c-8.84,0-16-7.16-16-16v-103.02c0-8.84,7.16-16,16-16s16,7.16,16,16v103.02c0,8.84-7.16,16-16,16Z'
fill='#fbbc04'
/>
<circle cx='256' cy='171' r='16' fill='#34a853' />
<circle cx='256' cy='235' r='16' fill='#34a853' />
<circle cx='320' cy='265' r='16' fill='#fbbc04' />
<circle cx='320' cy='329' r='16' fill='#fbbc04' />
<path
d='M192,217.36c-8.84,0-16-7.16-16-16v-100.02c0-8.84,7.16-16,16-16s16,7.16,16,16v100.02c0,8.84-7.16,16-16,16Z'
fill='#fbbc04'
/>
<circle cx='192' cy='265' r='16' fill='#fbbc04' />
<circle cx='192' cy='329' r='16' fill='#fbbc04' />
</g>
</svg>
)
export const CerebrasIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
{...props}
@@ -3335,6 +3385,21 @@ export function SalesforceIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ServiceNowIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 71.1 63.6'>
<path
fillRule='evenodd'
clipRule='evenodd'
fill='#62D84E'
d='M35.8,0C16.1,0,0,15.9,0,35.6c0,9.8,4,19.3,11.2,26c2.5,2.4,6.4,2.6,9.2,0.5c9-6.7,21.4-6.7,30.4,0
c2.8,2.1,6.7,1.9,9.2-0.5C74.3,48,74.9,25.4,61.3,11.1C54.7,4.1,45.4,0.1,35.8,0 M35.6,53.5C26,53.8,18,46.2,17.8,36.7
c0-0.3,0-0.6,0-0.9c0-9.8,8-17.8,17.8-17.8s17.8,8,17.8,17.8c0.3,9.6-7.3,17.5-16.8,17.8C36.2,53.5,35.9,53.5,35.6,53.5'
/>
</svg>
)
}
export function ApolloIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -85,6 +85,7 @@ import {
SendgridIcon,
SentryIcon,
SerperIcon,
ServiceNowIcon,
SftpIcon,
ShopifyIcon,
SlackIcon,
@@ -119,116 +120,117 @@ import {
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
export const blockTypeToIconMap: Record<string, IconComponent> = {
calendly: CalendlyIcon,
mailchimp: MailchimpIcon,
postgresql: PostgresIcon,
twilio_voice: TwilioIcon,
elasticsearch: ElasticsearchIcon,
rds: RDSIcon,
translate: TranslateIcon,
dynamodb: DynamoDBIcon,
wordpress: WordpressIcon,
tavily: TavilyIcon,
zendesk: ZendeskIcon,
youtube: YouTubeIcon,
supabase: SupabaseIcon,
vision: EyeIcon,
zoom: ZoomIcon,
confluence: ConfluenceIcon,
arxiv: ArxivIcon,
webflow: WebflowIcon,
pinecone: PineconeIcon,
apollo: ApolloIcon,
whatsapp: WhatsAppIcon,
typeform: TypeformIcon,
qdrant: QdrantIcon,
shopify: ShopifyIcon,
asana: AsanaIcon,
sqs: SQSIcon,
apify: ApifyIcon,
memory: BrainIcon,
gitlab: GitLabIcon,
polymarket: PolymarketIcon,
serper: SerperIcon,
linear: LinearIcon,
exa: ExaAIIcon,
telegram: TelegramIcon,
salesforce: SalesforceIcon,
hubspot: HubspotIcon,
hunter: HunterIOIcon,
linkup: LinkupIcon,
mongodb: MongoDBIcon,
airtable: AirtableIcon,
discord: DiscordIcon,
ahrefs: AhrefsIcon,
neo4j: Neo4jIcon,
tts: TTSIcon,
jina: JinaAIIcon,
google_docs: GoogleDocsIcon,
perplexity: PerplexityIcon,
google_search: GoogleIcon,
x: xIcon,
kalshi: KalshiIcon,
google_calendar: GoogleCalendarIcon,
zep: ZepIcon,
posthog: PosthogIcon,
grafana: GrafanaIcon,
google_slides: GoogleSlidesIcon,
microsoft_planner: MicrosoftPlannerIcon,
thinking: BrainIcon,
pipedrive: PipedriveIcon,
airtable: AirtableIcon,
apify: ApifyIcon,
apollo: ApolloIcon,
arxiv: ArxivIcon,
asana: AsanaIcon,
browser_use: BrowserUseIcon,
calendly: CalendlyIcon,
clay: ClayIcon,
confluence: ConfluenceIcon,
cursor: CursorIcon,
datadog: DatadogIcon,
discord: DiscordIcon,
dropbox: DropboxIcon,
stagehand: StagehandIcon,
google_forms: GoogleFormsIcon,
duckduckgo: DuckDuckGoIcon,
dynamodb: DynamoDBIcon,
elasticsearch: ElasticsearchIcon,
elevenlabs: ElevenLabsIcon,
exa: ExaAIIcon,
file: DocumentIcon,
mistral_parse: MistralIcon,
firecrawl: FirecrawlIcon,
github: GithubIcon,
gitlab: GitLabIcon,
gmail: GmailIcon,
google_calendar: GoogleCalendarIcon,
google_docs: GoogleDocsIcon,
google_drive: GoogleDriveIcon,
google_forms: GoogleFormsIcon,
google_groups: GoogleGroupsIcon,
google_search: GoogleIcon,
google_sheets: GoogleSheetsIcon,
google_slides: GoogleSlidesIcon,
google_vault: GoogleVaultIcon,
grafana: GrafanaIcon,
hubspot: HubspotIcon,
huggingface: HuggingFaceIcon,
hunter: HunterIOIcon,
image_generator: ImageIcon,
incidentio: IncidentioIcon,
intercom: IntercomIcon,
jina: JinaAIIcon,
jira: JiraIcon,
kalshi: KalshiIcon,
knowledge: PackageSearchIcon,
linear: LinearIcon,
linkedin: LinkedInIcon,
linkup: LinkupIcon,
mailchimp: MailchimpIcon,
mailgun: MailgunIcon,
mem0: Mem0Icon,
memory: BrainIcon,
microsoft_excel: MicrosoftExcelIcon,
microsoft_planner: MicrosoftPlannerIcon,
microsoft_teams: MicrosoftTeamsIcon,
mistral_parse: MistralIcon,
mongodb: MongoDBIcon,
mysql: MySQLIcon,
neo4j: Neo4jIcon,
notion: NotionIcon,
onedrive: MicrosoftOneDriveIcon,
openai: OpenAIIcon,
outlook: OutlookIcon,
incidentio: IncidentioIcon,
onedrive: MicrosoftOneDriveIcon,
resend: ResendIcon,
google_vault: GoogleVaultIcon,
sharepoint: MicrosoftSharepointIcon,
huggingface: HuggingFaceIcon,
sendgrid: SendgridIcon,
video_generator: VideoIcon,
smtp: SmtpIcon,
google_groups: GoogleGroupsIcon,
mailgun: MailgunIcon,
clay: ClayIcon,
jira: JiraIcon,
search: SearchIcon,
linkedin: LinkedInIcon,
wealthbox: WealthboxIcon,
notion: NotionIcon,
elevenlabs: ElevenLabsIcon,
microsoft_teams: MicrosoftTeamsIcon,
github: GithubIcon,
sftp: SftpIcon,
ssh: SshIcon,
google_drive: GoogleDriveIcon,
sentry: SentryIcon,
reddit: RedditIcon,
parallel_ai: ParallelIcon,
spotify: SpotifyIcon,
stripe: StripeIcon,
perplexity: PerplexityIcon,
pinecone: PineconeIcon,
pipedrive: PipedriveIcon,
polymarket: PolymarketIcon,
postgresql: PostgresIcon,
posthog: PosthogIcon,
qdrant: QdrantIcon,
rds: RDSIcon,
reddit: RedditIcon,
resend: ResendIcon,
s3: S3Icon,
trello: TrelloIcon,
mem0: Mem0Icon,
knowledge: PackageSearchIcon,
intercom: IntercomIcon,
twilio_sms: TwilioIcon,
duckduckgo: DuckDuckGoIcon,
salesforce: SalesforceIcon,
search: SearchIcon,
sendgrid: SendgridIcon,
sentry: SentryIcon,
serper: SerperIcon,
servicenow: ServiceNowIcon,
sftp: SftpIcon,
sharepoint: MicrosoftSharepointIcon,
shopify: ShopifyIcon,
slack: SlackIcon,
datadog: DatadogIcon,
microsoft_excel: MicrosoftExcelIcon,
image_generator: ImageIcon,
google_sheets: GoogleSheetsIcon,
wikipedia: WikipediaIcon,
cursor: CursorIcon,
firecrawl: FirecrawlIcon,
mysql: MySQLIcon,
browser_use: BrowserUseIcon,
smtp: SmtpIcon,
spotify: SpotifyIcon,
sqs: SQSIcon,
ssh: SshIcon,
stagehand: StagehandIcon,
stripe: StripeIcon,
stt: STTIcon,
supabase: SupabaseIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
thinking: BrainIcon,
translate: TranslateIcon,
trello: TrelloIcon,
tts: TTSIcon,
twilio_sms: TwilioIcon,
twilio_voice: TwilioIcon,
typeform: TypeformIcon,
video_generator: VideoIcon,
vision: EyeIcon,
wealthbox: WealthboxIcon,
webflow: WebflowIcon,
whatsapp: WhatsAppIcon,
wikipedia: WikipediaIcon,
wordpress: WordpressIcon,
x: xIcon,
youtube: YouTubeIcon,
zendesk: ZendeskIcon,
zep: ZepIcon,
zoom: ZoomIcon,
}

View File

@@ -111,26 +111,24 @@ Verschiedene Blocktypen erzeugen unterschiedliche Ausgabestrukturen. Hier ist, w
```json
{
"content": "Original content passed through",
"conditionResult": true,
"selectedPath": {
"blockId": "2acd9007-27e8-4510-a487-73d3b825e7c1",
"blockType": "agent",
"blockTitle": "Follow-up Agent"
},
"selectedConditionId": "condition-1"
"selectedOption": "condition-1"
}
```
### Ausgabefelder des Condition-Blocks
- **content**: Der ursprüngliche, durchgeleitete Inhalt
- **conditionResult**: Boolesches Ergebnis der Bedingungsauswertung
- **selectedPath**: Informationen über den ausgewählten Pfad
- **blockId**: ID des nächsten Blocks im ausgewählten Pfad
- **blockType**: Typ des nächsten Blocks
- **blockTitle**: Titel des nächsten Blocks
- **selectedConditionId**: ID der ausgewählten Bedingung
- **selectedOption**: ID der ausgewählten Bedingung
</Tab>
<Tab>

View File

@@ -90,14 +90,20 @@ Ein Jira-Issue erstellen
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Ja | Ihre Jira-Domain (z.B. ihrfirma.atlassian.net) |
| `domain` | string | Ja | Ihre Jira-Domain \(z.B. ihrfirma.atlassian.net\) |
| `projectId` | string | Ja | Projekt-ID für das Issue |
| `summary` | string | Ja | Zusammenfassung für das Issue |
| `description` | string | Nein | Beschreibung für das Issue |
| `priority` | string | Nein | Priorität für das Issue |
| `assignee` | string | Nein | Bearbeiter für das Issue |
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz. Wenn nicht angegeben, wird sie anhand der Domain abgerufen. |
| `issueType` | string | Ja | Art des zu erstellenden Issues (z.B. Task, Story) |
| `priority` | string | Nein | Prioritäts-ID oder -Name für das Issue \(z.B. "10000" oder "High"\) |
| `assignee` | string | Nein | Account-ID des Bearbeiters für das Issue |
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz. Wenn nicht angegeben, wird sie über die Domain abgerufen. |
| `issueType` | string | Ja | Typ des zu erstellenden Issues \(z.B. Task, Story\) |
| `labels` | array | Nein | Labels für das Issue \(Array von Label-Namen\) |
| `duedate` | string | Nein | Fälligkeitsdatum für das Issue \(Format: YYYY-MM-DD\) |
| `reporter` | string | Nein | Account-ID des Melders für das Issue |
| `environment` | string | Nein | Umgebungsinformationen für das Issue |
| `customFieldId` | string | Nein | Benutzerdefinierte Feld-ID \(z.B. customfield_10001\) |
| `customFieldValue` | string | Nein | Wert für das benutzerdefinierte Feld |
#### Ausgabe
@@ -107,6 +113,7 @@ Ein Jira-Issue erstellen
| `issueKey` | string | Erstellter Issue-Key \(z.B. PROJ-123\) |
| `summary` | string | Issue-Zusammenfassung |
| `url` | string | URL zum erstellten Issue |
| `assigneeId` | string | Account-ID des zugewiesenen Benutzers \(falls zugewiesen\) |
### `jira_bulk_read`
@@ -520,6 +527,30 @@ Einen Beobachter von einem Jira-Issue entfernen
| `issueKey` | string | Issue-Key |
| `watcherAccountId` | string | Account-ID des entfernten Beobachters |
### `jira_get_users`
Jira-Benutzer abrufen. Wenn eine Account-ID angegeben wird, wird ein einzelner Benutzer zurückgegeben. Andernfalls wird eine Liste aller Benutzer zurückgegeben.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Ja | Ihre Jira-Domain \(z.B. ihrfirma.atlassian.net\) |
| `accountId` | string | Nein | Optionale Account-ID, um einen bestimmten Benutzer abzurufen. Wenn nicht angegeben, werden alle Benutzer zurückgegeben. |
| `startAt` | number | Nein | Der Index des ersten zurückzugebenden Benutzers \(für Paginierung, Standard: 0\) |
| `maxResults` | number | Nein | Maximale Anzahl der zurückzugebenden Benutzer \(Standard: 50\) |
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz. Wenn nicht angegeben, wird sie anhand der Domain abgerufen. |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `ts` | string | Zeitstempel der Operation |
| `users` | json | Array von Benutzern mit accountId, displayName, emailAddress, active-Status und avatarUrls |
| `total` | number | Gesamtanzahl der zurückgegebenen Benutzer |
| `startAt` | number | Startindex für Paginierung |
| `maxResults` | number | Maximale Ergebnisse pro Seite |
## Hinweise
- Kategorie: `tools`

View File

@@ -0,0 +1,124 @@
---
title: ServiceNow
description: ServiceNow-Datensätze erstellen, lesen, aktualisieren und löschen
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="servicenow"
color="#032D42"
/>
{/* MANUAL-CONTENT-START:intro */}
[ServiceNow](https://www.servicenow.com/) ist eine leistungsstarke Cloud-Plattform zur Optimierung und Automatisierung von IT-Service-Management (ITSM), Workflows und Geschäftsprozessen in Ihrem Unternehmen. ServiceNow ermöglicht Ihnen die Verwaltung von Vorfällen, Anfragen, Aufgaben, Benutzern und mehr über seine umfangreiche API.
Mit ServiceNow können Sie:
- **IT-Workflows automatisieren**: Datensätze in jeder ServiceNow-Tabelle erstellen, lesen, aktualisieren und löschen, z. B. Vorfälle, Aufgaben, Änderungsanfragen und Benutzer.
- **Systeme integrieren**: ServiceNow mit Ihren anderen Tools und Prozessen für nahtlose Automatisierung verbinden.
- **Eine einzige Informationsquelle pflegen**: Alle Ihre Service- und Betriebsdaten organisiert und zugänglich halten.
- **Betriebliche Effizienz steigern**: Manuelle Arbeit reduzieren und Servicequalität mit anpassbaren Workflows und Automatisierung verbessern.
In Sim ermöglicht die ServiceNow-Integration Ihren Agenten, direkt mit Ihrer ServiceNow-Instanz als Teil ihrer Workflows zu interagieren. Agenten können Datensätze in jeder ServiceNow-Tabelle erstellen, lesen, aktualisieren oder löschen und Ticket- oder Benutzerdaten für ausgefeilte Automatisierung und Entscheidungsfindung nutzen. Diese Integration verbindet Ihre Workflow-Automatisierung und IT-Betrieb und befähigt Ihre Agenten, Serviceanfragen, Vorfälle, Benutzer und Assets ohne manuelle Eingriffe zu verwalten. Durch die Verbindung von Sim mit ServiceNow können Sie Service-Management-Aufgaben automatisieren, Reaktionszeiten verbessern und konsistenten, sicheren Zugriff auf die wichtigen Servicedaten Ihres Unternehmens gewährleisten.
{/* MANUAL-CONTENT-END */}
## Nutzungsanweisungen
Integrieren Sie ServiceNow in Ihren Workflow. Erstellen, lesen, aktualisieren und löschen Sie Datensätze in jeder ServiceNow-Tabelle, einschließlich Vorfälle, Aufgaben, Änderungsanfragen, Benutzer und mehr.
## Tools
### `servicenow_create_record`
Einen neuen Datensatz in einer ServiceNow-Tabelle erstellen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Ja | ServiceNow-Instanz-URL \(z. B. https://instance.service-now.com\) |
| `username` | string | Ja | ServiceNow-Benutzername |
| `password` | string | Ja | ServiceNow-Passwort |
| `tableName` | string | Ja | Tabellenname \(z. B. incident, task, sys_user\) |
| `fields` | json | Ja | Felder, die für den Datensatz festgelegt werden sollen \(JSON-Objekt\) |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `record` | json | Erstellter ServiceNow-Datensatz mit sys_id und anderen Feldern |
| `metadata` | json | Metadaten der Operation |
### `servicenow_read_record`
Datensätze aus einer ServiceNow-Tabelle lesen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Ja | ServiceNow-Instanz-URL \(z. B. https://instance.service-now.com\) |
| `username` | string | Ja | ServiceNow-Benutzername |
| `password` | string | Ja | ServiceNow-Passwort |
| `tableName` | string | Ja | Tabellenname |
| `sysId` | string | Nein | Spezifische Datensatz-sys_id |
| `number` | string | Nein | Datensatznummer \(z. B. INC0010001\) |
| `query` | string | Nein | Kodierte Abfragezeichenfolge \(z. B. "active=true^priority=1"\) |
| `limit` | number | Nein | Maximale Anzahl der zurückzugebenden Datensätze |
| `fields` | string | Nein | Durch Kommas getrennte Liste der zurückzugebenden Felder |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `records` | array | Array von ServiceNow-Datensätzen |
| `metadata` | json | Metadaten der Operation |
### `servicenow_update_record`
Einen bestehenden Datensatz in einer ServiceNow-Tabelle aktualisieren
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Ja | ServiceNow-Instanz-URL \(z. B. https://instance.service-now.com\) |
| `username` | string | Ja | ServiceNow-Benutzername |
| `password` | string | Ja | ServiceNow-Passwort |
| `tableName` | string | Ja | Tabellenname |
| `sysId` | string | Ja | Datensatz-sys_id zum Aktualisieren |
| `fields` | json | Ja | Zu aktualisierende Felder \(JSON-Objekt\) |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `record` | json | Aktualisierter ServiceNow-Datensatz |
| `metadata` | json | Metadaten der Operation |
### `servicenow_delete_record`
Einen Datensatz aus einer ServiceNow-Tabelle löschen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Ja | ServiceNow-Instanz-URL \(z. B. https://instance.service-now.com\) |
| `username` | string | Ja | ServiceNow-Benutzername |
| `password` | string | Ja | ServiceNow-Passwort |
| `tableName` | string | Ja | Tabellenname |
| `sysId` | string | Ja | Datensatz-sys_id zum Löschen |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Ob das Löschen erfolgreich war |
| `metadata` | json | Metadaten der Operation |
## Hinweise
- Kategorie: `tools`
- Typ: `servicenow`

View File

@@ -109,12 +109,12 @@ Lesen Sie die neuesten Nachrichten aus Slack-Kanälen. Rufen Sie den Konversatio
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | Nein | Authentifizierungsmethode: oauth oder bot_token |
| `botToken` | string | Nein | Bot-Token für benutzerdefinierten Bot |
| `botToken` | string | Nein | Bot-Token für Custom Bot |
| `channel` | string | Nein | Slack-Kanal, aus dem Nachrichten gelesen werden sollen \(z.B. #general\) |
| `userId` | string | Nein | Benutzer-ID für DM-Konversation \(z.B. U1234567890\) |
| `limit` | number | Nein | Anzahl der abzurufenden Nachrichten \(Standard: 10, max: 100\) |
| `oldest` | string | Nein | Beginn des Zeitraums \(Zeitstempel\) |
| `latest` | string | Nein | Ende des Zeitraums \(Zeitstempel\) |
| `limit` | number | Nein | Anzahl der abzurufenden Nachrichten \(Standard: 10, max: 15\) |
| `oldest` | string | Nein | Beginn des Zeitbereichs \(Zeitstempel\) |
| `latest` | string | Nein | Ende des Zeitbereichs \(Zeitstempel\) |
#### Ausgabe

View File

@@ -39,14 +39,16 @@ Senden Sie eine Chat-Completion-Anfrage an jeden unterstützten LLM-Anbieter
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `model` | string | Ja | Das zu verwendende Modell (z.B. gpt-4o, claude-sonnet-4-5, gemini-2.0-flash) |
| `systemPrompt` | string | Nein | System-Prompt zur Festlegung des Assistentenverhaltens |
| `context` | string | Ja | Die Benutzernachricht oder der Kontext, der an das Modell gesendet wird |
| `apiKey` | string | Nein | API-Schlüssel für den Anbieter (verwendet den Plattformschlüssel, wenn für gehostete Modelle nicht angegeben) |
| `temperature` | number | Nein | Temperatur für die Antwortgenerierung (0-2) |
| `maxTokens` | number | Nein | Maximale Tokens in der Antwort |
| `model` | string | Ja | Das zu verwendende Modell \(z. B. gpt-4o, claude-sonnet-4-5, gemini-2.0-flash\) |
| `systemPrompt` | string | Nein | System-Prompt zur Festlegung des Verhaltens des Assistenten |
| `context` | string | Ja | Die Benutzernachricht oder der Kontext, der an das Modell gesendet werden soll |
| `apiKey` | string | Nein | API-Schlüssel für den Anbieter \(verwendet Plattform-Schlüssel, falls nicht für gehostete Modelle angegeben\) |
| `temperature` | number | Nein | Temperatur für die Antwortgenerierung \(0-2\) |
| `maxTokens` | number | Nein | Maximale Anzahl von Tokens in der Antwort |
| `azureEndpoint` | string | Nein | Azure OpenAI-Endpunkt-URL |
| `azureApiVersion` | string | Nein | Azure OpenAI API-Version |
| `azureApiVersion` | string | Nein | Azure OpenAI-API-Version |
| `vertexProject` | string | Nein | Google Cloud-Projekt-ID für Vertex AI |
| `vertexLocation` | string | Nein | Google Cloud-Standort für Vertex AI \(Standard: us-central1\) |
#### Ausgabe

View File

@@ -106,26 +106,24 @@ Different block types produce different output structures. Here's what you can e
<Tab>
```json
{
"content": "Original content passed through",
"conditionResult": true,
"selectedPath": {
"blockId": "2acd9007-27e8-4510-a487-73d3b825e7c1",
"blockType": "agent",
"blockTitle": "Follow-up Agent"
},
"selectedConditionId": "condition-1"
"selectedOption": "condition-1"
}
```
### Condition Block Output Fields
- **content**: The original content passed through
- **conditionResult**: Boolean result of the condition evaluation
- **selectedPath**: Information about the selected path
- **blockId**: ID of the next block in the selected path
- **blockType**: Type of the next block
- **blockTitle**: Title of the next block
- **selectedConditionId**: ID of the selected condition
- **selectedOption**: ID of the selected condition
</Tab>
<Tab>

View File

@@ -97,10 +97,16 @@ Write a Jira issue
| `projectId` | string | Yes | Project ID for the issue |
| `summary` | string | Yes | Summary for the issue |
| `description` | string | No | Description for the issue |
| `priority` | string | No | Priority for the issue |
| `assignee` | string | No | Assignee for the issue |
| `priority` | string | No | Priority ID or name for the issue \(e.g., "10000" or "High"\) |
| `assignee` | string | No | Assignee account ID for the issue |
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
| `issueType` | string | Yes | Type of issue to create \(e.g., Task, Story\) |
| `labels` | array | No | Labels for the issue \(array of label names\) |
| `duedate` | string | No | Due date for the issue \(format: YYYY-MM-DD\) |
| `reporter` | string | No | Reporter account ID for the issue |
| `environment` | string | No | Environment information for the issue |
| `customFieldId` | string | No | Custom field ID \(e.g., customfield_10001\) |
| `customFieldValue` | string | No | Value for the custom field |
#### Output
@@ -110,6 +116,7 @@ Write a Jira issue
| `issueKey` | string | Created issue key \(e.g., PROJ-123\) |
| `summary` | string | Issue summary |
| `url` | string | URL to the created issue |
| `assigneeId` | string | Account ID of the assigned user \(if assigned\) |
### `jira_bulk_read`
@@ -523,6 +530,30 @@ Remove a watcher from a Jira issue
| `issueKey` | string | Issue key |
| `watcherAccountId` | string | Removed watcher account ID |
### `jira_get_users`
Get Jira users. If an account ID is provided, returns a single user. Otherwise, returns a list of all users.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `accountId` | string | No | Optional account ID to get a specific user. If not provided, returns all users. |
| `startAt` | number | No | The index of the first user to return \(for pagination, default: 0\) |
| `maxResults` | number | No | Maximum number of users to return \(default: 50\) |
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `users` | json | Array of users with accountId, displayName, emailAddress, active status, and avatarUrls |
| `total` | number | Total number of users returned |
| `startAt` | number | Pagination start index |
| `maxResults` | number | Maximum results per page |
## Notes

View File

@@ -80,6 +80,7 @@
"sendgrid",
"sentry",
"serper",
"servicenow",
"sftp",
"sharepoint",
"shopify",

View File

@@ -0,0 +1,129 @@
---
title: ServiceNow
description: Create, read, update, and delete ServiceNow records
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="servicenow"
color="#032D42"
/>
{/* MANUAL-CONTENT-START:intro */}
[ServiceNow](https://www.servicenow.com/) is a powerful cloud platform designed to streamline and automate IT service management (ITSM), workflows, and business processes across your organization. ServiceNow enables you to manage incidents, requests, tasks, users, and more using its extensive API.
With ServiceNow, you can:
- **Automate IT workflows**: Create, read, update, and delete records in any ServiceNow table, such as incidents, tasks, change requests, and users.
- **Integrate systems**: Connect ServiceNow with your other tools and processes for seamless automation.
- **Maintain a single source of truth**: Keep all your service and operations data organized and accessible.
- **Drive operational efficiency**: Reduce manual work and improve service quality with customizable workflows and automation.
In Sim, the ServiceNow integration enables your agents to interact directly with your ServiceNow instance as part of their workflows. Agents can create, read, update, or delete records in any ServiceNow table and leverage ticket or user data for sophisticated automation and decision-making. This integration bridges your workflow automation and IT operations, empowering your agents to manage service requests, incidents, users, and assets without manual intervention. By connecting Sim with ServiceNow, you can automate service management tasks, improve response times, and ensure consistent, secure access to your organization's vital service data.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate ServiceNow into your workflow. Create, read, update, and delete records in any ServiceNow table including incidents, tasks, change requests, users, and more.
## Tools
### `servicenow_create_record`
Create a new record in a ServiceNow table
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) |
| `username` | string | Yes | ServiceNow username |
| `password` | string | Yes | ServiceNow password |
| `tableName` | string | Yes | Table name \(e.g., incident, task, sys_user\) |
| `fields` | json | Yes | Fields to set on the record \(JSON object\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `record` | json | Created ServiceNow record with sys_id and other fields |
| `metadata` | json | Operation metadata |
### `servicenow_read_record`
Read records from a ServiceNow table
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) |
| `username` | string | Yes | ServiceNow username |
| `password` | string | Yes | ServiceNow password |
| `tableName` | string | Yes | Table name |
| `sysId` | string | No | Specific record sys_id |
| `number` | string | No | Record number \(e.g., INC0010001\) |
| `query` | string | No | Encoded query string \(e.g., "active=true^priority=1"\) |
| `limit` | number | No | Maximum number of records to return |
| `fields` | string | No | Comma-separated list of fields to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `records` | array | Array of ServiceNow records |
| `metadata` | json | Operation metadata |
### `servicenow_update_record`
Update an existing record in a ServiceNow table
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) |
| `username` | string | Yes | ServiceNow username |
| `password` | string | Yes | ServiceNow password |
| `tableName` | string | Yes | Table name |
| `sysId` | string | Yes | Record sys_id to update |
| `fields` | json | Yes | Fields to update \(JSON object\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `record` | json | Updated ServiceNow record |
| `metadata` | json | Operation metadata |
### `servicenow_delete_record`
Delete a record from a ServiceNow table
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) |
| `username` | string | Yes | ServiceNow username |
| `password` | string | Yes | ServiceNow password |
| `tableName` | string | Yes | Table name |
| `sysId` | string | Yes | Record sys_id to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the deletion was successful |
| `metadata` | json | Operation metadata |
## Notes
- Category: `tools`
- Type: `servicenow`

View File

@@ -114,7 +114,7 @@ Read the latest messages from Slack channels. Retrieve conversation history with
| `botToken` | string | No | Bot token for Custom Bot |
| `channel` | string | No | Slack channel to read messages from \(e.g., #general\) |
| `userId` | string | No | User ID for DM conversation \(e.g., U1234567890\) |
| `limit` | number | No | Number of messages to retrieve \(default: 10, max: 100\) |
| `limit` | number | No | Number of messages to retrieve \(default: 10, max: 15\) |
| `oldest` | string | No | Start of time range \(timestamp\) |
| `latest` | string | No | End of time range \(timestamp\) |

View File

@@ -50,6 +50,8 @@ Send a chat completion request to any supported LLM provider
| `maxTokens` | number | No | Maximum tokens in the response |
| `azureEndpoint` | string | No | Azure OpenAI endpoint URL |
| `azureApiVersion` | string | No | Azure OpenAI API version |
| `vertexProject` | string | No | Google Cloud project ID for Vertex AI |
| `vertexLocation` | string | No | Google Cloud location for Vertex AI \(defaults to us-central1\) |
#### Output

View File

@@ -111,26 +111,24 @@ Diferentes tipos de bloques producen diferentes estructuras de salida. Esto es l
```json
{
"content": "Original content passed through",
"conditionResult": true,
"selectedPath": {
"blockId": "2acd9007-27e8-4510-a487-73d3b825e7c1",
"blockType": "agent",
"blockTitle": "Follow-up Agent"
},
"selectedConditionId": "condition-1"
"selectedOption": "condition-1"
}
```
### Campos de salida del bloque de condición
- **content**: El contenido original que se transmite
- **conditionResult**: Resultado booleano de la evaluación de la condición
- **selectedPath**: Información sobre la ruta seleccionada
- **conditionResult**: resultado booleano de la evaluación de la condición
- **selectedPath**: información sobre la ruta seleccionada
- **blockId**: ID del siguiente bloque en la ruta seleccionada
- **blockType**: Tipo del siguiente bloque
- **blockTitle**: Título del siguiente bloque
- **selectedConditionId**: ID de la condición seleccionada
- **blockType**: tipo del siguiente bloque
- **blockTitle**: título del siguiente bloque
- **selectedOption**: ID de la condición seleccionada
</Tab>
<Tab>

View File

@@ -89,24 +89,31 @@ Escribir una incidencia de Jira
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| --------- | ---- | ----------- | ----------- |
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
| `projectId` | string | Sí | ID del proyecto para la incidencia |
| `summary` | string | Sí | Resumen de la incidencia |
| `description` | string | No | Descripción de la incidencia |
| `priority` | string | No | Prioridad de la incidencia |
| `assignee` | string | No | Asignado para la incidencia |
| `cloudId` | string | No | ID de Jira Cloud para la instancia. Si no se proporciona, se obtendrá utilizando el dominio. |
| `priority` | string | No | ID o nombre de prioridad para la incidencia \(p. ej., "10000" o "Alta"\) |
| `assignee` | string | No | ID de cuenta del asignado para la incidencia |
| `cloudId` | string | No | ID de Jira Cloud para la instancia. Si no se proporciona, se obtendrá usando el dominio. |
| `issueType` | string | Sí | Tipo de incidencia a crear \(p. ej., Tarea, Historia\) |
| `labels` | array | No | Etiquetas para la incidencia \(array de nombres de etiquetas\) |
| `duedate` | string | No | Fecha de vencimiento para la incidencia \(formato: AAAA-MM-DD\) |
| `reporter` | string | No | ID de cuenta del informador para la incidencia |
| `environment` | string | No | Información del entorno para la incidencia |
| `customFieldId` | string | No | ID del campo personalizado \(p. ej., customfield_10001\) |
| `customFieldValue` | string | No | Valor para el campo personalizado |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `ts` | string | Marca de tiempo de la operación |
| `issueKey` | string | Clave de la incidencia creada (p. ej., PROJ-123) |
| `issueKey` | string | Clave de la incidencia creada \(p. ej., PROJ-123\) |
| `summary` | string | Resumen de la incidencia |
| `url` | string | URL de la incidencia creada |
| `assigneeId` | string | ID de cuenta del usuario asignado \(si está asignado\) |
### `jira_bulk_read`
@@ -520,6 +527,30 @@ Eliminar un observador de una incidencia de Jira
| `issueKey` | string | Clave de incidencia |
| `watcherAccountId` | string | ID de cuenta del observador eliminado |
### `jira_get_users`
Obtener usuarios de Jira. Si se proporciona un ID de cuenta, devuelve un solo usuario. De lo contrario, devuelve una lista de todos los usuarios.
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
| `accountId` | string | No | ID de cuenta opcional para obtener un usuario específico. Si no se proporciona, devuelve todos los usuarios. |
| `startAt` | number | No | El índice del primer usuario a devolver \(para paginación, predeterminado: 0\) |
| `maxResults` | number | No | Número máximo de usuarios a devolver \(predeterminado: 50\) |
| `cloudId` | string | No | ID de Jira Cloud para la instancia. Si no se proporciona, se obtendrá usando el dominio. |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `ts` | string | Marca de tiempo de la operación |
| `users` | json | Array de usuarios con accountId, displayName, emailAddress, estado activo y avatarUrls |
| `total` | number | Número total de usuarios devueltos |
| `startAt` | number | Índice de inicio de paginación |
| `maxResults` | number | Máximo de resultados por página |
## Notas
- Categoría: `tools`

View File

@@ -0,0 +1,124 @@
---
title: ServiceNow
description: Crear, leer, actualizar y eliminar registros de ServiceNow
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="servicenow"
color="#032D42"
/>
{/* MANUAL-CONTENT-START:intro */}
[ServiceNow](https://www.servicenow.com/) es una potente plataforma en la nube diseñada para optimizar y automatizar la gestión de servicios de TI (ITSM), flujos de trabajo y procesos empresariales en toda tu organización. ServiceNow te permite gestionar incidencias, solicitudes, tareas, usuarios y más utilizando su amplia API.
Con ServiceNow, puedes:
- **Automatizar flujos de trabajo de TI**: crear, leer, actualizar y eliminar registros en cualquier tabla de ServiceNow, como incidencias, tareas, solicitudes de cambio y usuarios.
- **Integrar sistemas**: conectar ServiceNow con tus otras herramientas y procesos para una automatización fluida.
- **Mantener una única fuente de verdad**: mantener todos tus datos de servicio y operaciones organizados y accesibles.
- **Impulsar la eficiencia operativa**: reducir el trabajo manual y mejorar la calidad del servicio con flujos de trabajo personalizables y automatización.
En Sim, la integración de ServiceNow permite que tus agentes interactúen directamente con tu instancia de ServiceNow como parte de sus flujos de trabajo. Los agentes pueden crear, leer, actualizar o eliminar registros en cualquier tabla de ServiceNow y aprovechar datos de tickets o usuarios para automatización y toma de decisiones sofisticadas. Esta integración conecta tu automatización de flujos de trabajo y operaciones de TI, permitiendo que tus agentes gestionen solicitudes de servicio, incidencias, usuarios y activos sin intervención manual. Al conectar Sim con ServiceNow, puedes automatizar tareas de gestión de servicios, mejorar los tiempos de respuesta y garantizar un acceso consistente y seguro a los datos de servicio vitales de tu organización.
{/* MANUAL-CONTENT-END */}
## Instrucciones de uso
Integra ServiceNow en tu flujo de trabajo. Crea, lee, actualiza y elimina registros en cualquier tabla de ServiceNow, incluyendo incidencias, tareas, solicitudes de cambio, usuarios y más.
## Herramientas
### `servicenow_create_record`
Crear un nuevo registro en una tabla de ServiceNow
#### Entrada
| Parámetro | Tipo | Requerido | Descripción |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Sí | URL de la instancia de ServiceNow \(p. ej., https://instance.service-now.com\) |
| `username` | string | Sí | Nombre de usuario de ServiceNow |
| `password` | string | Sí | Contraseña de ServiceNow |
| `tableName` | string | Sí | Nombre de la tabla \(p. ej., incident, task, sys_user\) |
| `fields` | json | Sí | Campos a establecer en el registro \(objeto JSON\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `record` | json | Registro de ServiceNow creado con sys_id y otros campos |
| `metadata` | json | Metadatos de la operación |
### `servicenow_read_record`
Leer registros de una tabla de ServiceNow
#### Entrada
| Parámetro | Tipo | Requerido | Descripción |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Sí | URL de la instancia de ServiceNow \(p. ej., https://instance.service-now.com\) |
| `username` | string | Sí | Nombre de usuario de ServiceNow |
| `password` | string | Sí | Contraseña de ServiceNow |
| `tableName` | string | Sí | Nombre de la tabla |
| `sysId` | string | No | sys_id del registro específico |
| `number` | string | No | Número de registro \(p. ej., INC0010001\) |
| `query` | string | No | Cadena de consulta codificada \(p. ej., "active=true^priority=1"\) |
| `limit` | number | No | Número máximo de registros a devolver |
| `fields` | string | No | Lista de campos separados por comas a devolver |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `records` | array | Array de registros de ServiceNow |
| `metadata` | json | Metadatos de la operación |
### `servicenow_update_record`
Actualiza un registro existente en una tabla de ServiceNow
#### Entrada
| Parámetro | Tipo | Requerido | Descripción |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Sí | URL de la instancia de ServiceNow \(ej., https://instance.service-now.com\) |
| `username` | string | Sí | Nombre de usuario de ServiceNow |
| `password` | string | Sí | Contraseña de ServiceNow |
| `tableName` | string | Sí | Nombre de la tabla |
| `sysId` | string | Sí | sys_id del registro a actualizar |
| `fields` | json | Sí | Campos a actualizar \(objeto JSON\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `record` | json | Registro de ServiceNow actualizado |
| `metadata` | json | Metadatos de la operación |
### `servicenow_delete_record`
Elimina un registro de una tabla de ServiceNow
#### Entrada
| Parámetro | Tipo | Requerido | Descripción |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Sí | URL de la instancia de ServiceNow \(ej., https://instance.service-now.com\) |
| `username` | string | Sí | Nombre de usuario de ServiceNow |
| `password` | string | Sí | Contraseña de ServiceNow |
| `tableName` | string | Sí | Nombre de la tabla |
| `sysId` | string | Sí | sys_id del registro a eliminar |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Si la eliminación fue exitosa |
| `metadata` | json | Metadatos de la operación |
## Notas
- Categoría: `tools`
- Tipo: `servicenow`

View File

@@ -111,8 +111,8 @@ Lee los últimos mensajes de los canales de Slack. Recupera el historial de conv
| `authMethod` | string | No | Método de autenticación: oauth o bot_token |
| `botToken` | string | No | Token del bot para Bot personalizado |
| `channel` | string | No | Canal de Slack del que leer mensajes (p. ej., #general) |
| `userId` | string | No | ID de usuario para conversación por MD (p. ej., U1234567890) |
| `limit` | number | No | Número de mensajes a recuperar (predeterminado: 10, máx: 100) |
| `userId` | string | No | ID de usuario para conversación de mensaje directo (p. ej., U1234567890) |
| `limit` | number | No | Número de mensajes a recuperar (predeterminado: 10, máx: 15) |
| `oldest` | string | No | Inicio del rango de tiempo (marca de tiempo) |
| `latest` | string | No | Fin del rango de tiempo (marca de tiempo) |

View File

@@ -37,16 +37,18 @@ Envía una solicitud de completado de chat a cualquier proveedor de LLM compatib
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| Parámetro | Tipo | Requerido | Descripción |
| --------- | ---- | -------- | ----------- |
| `model` | string | Sí | El modelo a utilizar \(p. ej., gpt-4o, claude-sonnet-4-5, gemini-2.0-flash\) |
| `model` | string | Sí | El modelo a utilizar \(ej., gpt-4o, claude-sonnet-4-5, gemini-2.0-flash\) |
| `systemPrompt` | string | No | Prompt del sistema para establecer el comportamiento del asistente |
| `context` | string | Sí | El mensaje del usuario o contexto para enviar al modelo |
| `apiKey` | string | No | Clave API para el proveedor \(usa la clave de la plataforma si no se proporciona para modelos alojados\) |
| `context` | string | Sí | El mensaje del usuario o contexto a enviar al modelo |
| `apiKey` | string | No | Clave API del proveedor \(usa la clave de la plataforma si no se proporciona para modelos alojados\) |
| `temperature` | number | No | Temperatura para la generación de respuestas \(0-2\) |
| `maxTokens` | number | No | Tokens máximos en la respuesta |
| `azureEndpoint` | string | No | URL del endpoint de Azure OpenAI |
| `azureApiVersion` | string | No | Versión de la API de Azure OpenAI |
| `vertexProject` | string | No | ID del proyecto de Google Cloud para Vertex AI |
| `vertexLocation` | string | No | Ubicación de Google Cloud para Vertex AI \(por defecto us-central1\) |
#### Salida

View File

@@ -111,26 +111,24 @@ Différents types de blocs produisent différentes structures de sortie. Voici c
```json
{
"content": "Original content passed through",
"conditionResult": true,
"selectedPath": {
"blockId": "2acd9007-27e8-4510-a487-73d3b825e7c1",
"blockType": "agent",
"blockTitle": "Follow-up Agent"
},
"selectedConditionId": "condition-1"
"selectedOption": "condition-1"
}
```
### Champs de sortie du bloc de condition
- **content** : le contenu original transmis
- **conditionResult** : résultat booléen de l'évaluation de la condition
- **selectedPath** : informations sur le chemin sélectionné
- **blockId** : ID du bloc suivant dans le chemin sélectionné
- **blockType** : type du bloc suivant
- **blockTitle** : titre du bloc suivant
- **selectedConditionId** : ID de la condition sélectionnée
- **selectedOption** : ID de la condition sélectionnée
</Tab>
<Tab>

View File

@@ -89,15 +89,21 @@ Rédiger une demande Jira
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Oui | Votre domaine Jira (ex. : votreentreprise.atlassian.net) |
| `projectId` | string | Oui | ID du projet pour la demande |
| `summary` | string | Oui | Résumé de la demande |
| `description` | string | Non | Description de la demande |
| `priority` | string | Non | Priorité de la demande |
| `assignee` | string | Non | Assigné de la demande |
| `cloudId` | string | Non | ID Jira Cloud pour l'instance. S'il n'est pas fourni, il sera récupéré à l'aide du domaine. |
| `issueType` | string | Oui | Type de demande à créer (ex. : Tâche, Story) |
| --------- | ---- | ----------- | ----------- |
| `domain` | chaîne | Oui | Votre domaine Jira \(ex. : votreentreprise.atlassian.net\) |
| `projectId` | chaîne | Oui | ID du projet pour le ticket |
| `summary` | chaîne | Oui | Résumé du ticket |
| `description` | chaîne | Non | Description du ticket |
| `priority` | chaîne | Non | ID ou nom de la priorité du ticket \(ex. : "10000" ou "Haute"\) |
| `assignee` | chaîne | Non | ID de compte de l'assigné pour le ticket |
| `cloudId` | chaîne | Non | ID Cloud Jira pour l'instance. S'il n'est pas fourni, il sera récupéré à l'aide du domaine. |
| `issueType` | chaîne | Oui | Type de ticket à créer \(ex. : tâche, story\) |
| `labels` | tableau | Non | Étiquettes pour le ticket \(tableau de noms d'étiquettes\) |
| `duedate` | chaîne | Non | Date d'échéance du ticket \(format : AAAA-MM-JJ\) |
| `reporter` | chaîne | Non | ID de compte du rapporteur pour le ticket |
| `environment` | chaîne | Non | Informations d'environnement pour le ticket |
| `customFieldId` | chaîne | Non | ID du champ personnalisé \(ex. : customfield_10001\) |
| `customFieldValue` | chaîne | Non | Valeur pour le champ personnalisé |
#### Sortie
@@ -107,6 +113,7 @@ Rédiger une demande Jira
| `issueKey` | chaîne | Clé du ticket créé \(ex. : PROJ-123\) |
| `summary` | chaîne | Résumé du ticket |
| `url` | chaîne | URL vers le ticket créé |
| `assigneeId` | chaîne | ID de compte de l'utilisateur assigné \(si assigné\) |
### `jira_bulk_read`
@@ -520,7 +527,31 @@ Supprimer un observateur d'un ticket Jira
| `issueKey` | string | Clé du ticket |
| `watcherAccountId` | string | ID du compte observateur supprimé |
## Notes
### `jira_get_users`
Récupère les utilisateurs Jira. Si un ID de compte est fourni, renvoie un seul utilisateur. Sinon, renvoie une liste de tous les utilisateurs.
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `domain` | chaîne | Oui | Votre domaine Jira \(ex. : votreentreprise.atlassian.net\) |
| `accountId` | chaîne | Non | ID de compte optionnel pour obtenir un utilisateur spécifique. S'il n'est pas fourni, renvoie tous les utilisateurs. |
| `startAt` | nombre | Non | L'index du premier utilisateur à renvoyer \(pour la pagination, par défaut : 0\) |
| `maxResults` | nombre | Non | Nombre maximum d'utilisateurs à renvoyer \(par défaut : 50\) |
| `cloudId` | chaîne | Non | ID Cloud Jira pour l'instance. S'il n'est pas fourni, il sera récupéré à l'aide du domaine. |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `ts` | chaîne | Horodatage de l'opération |
| `users` | json | Tableau d'utilisateurs avec accountId, displayName, emailAddress, statut actif et avatarUrls |
| `total` | nombre | Nombre total d'utilisateurs renvoyés |
| `startAt` | nombre | Index de début de pagination |
| `maxResults` | nombre | Nombre maximum de résultats par page |
## Remarques
- Catégorie : `tools`
- Type : `jira`

View File

@@ -0,0 +1,124 @@
---
title: ServiceNow
description: Créer, lire, mettre à jour et supprimer des enregistrements ServiceNow
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="servicenow"
color="#032D42"
/>
{/* MANUAL-CONTENT-START:intro */}
[ServiceNow](https://www.servicenow.com/) est une plateforme cloud puissante conçue pour rationaliser et automatiser la gestion des services informatiques (ITSM), les workflows et les processus métier au sein de votre organisation. ServiceNow vous permet de gérer les incidents, les demandes, les tâches, les utilisateurs et bien plus encore grâce à son API étendue.
Avec ServiceNow, vous pouvez :
- **Automatiser les workflows informatiques** : créer, lire, mettre à jour et supprimer des enregistrements dans n'importe quelle table ServiceNow, tels que les incidents, les tâches, les demandes de changement et les utilisateurs.
- **Intégrer les systèmes** : connecter ServiceNow avec vos autres outils et processus pour une automatisation transparente.
- **Maintenir une source unique de vérité** : garder toutes vos données de service et d'exploitation organisées et accessibles.
- **Améliorer l'efficacité opérationnelle** : réduire le travail manuel et améliorer la qualité du service grâce à des workflows personnalisables et à l'automatisation.
Dans Sim, l'intégration ServiceNow permet à vos agents d'interagir directement avec votre instance ServiceNow dans le cadre de leurs workflows. Les agents peuvent créer, lire, mettre à jour ou supprimer des enregistrements dans n'importe quelle table ServiceNow et exploiter les données de tickets ou d'utilisateurs pour une automatisation et une prise de décision sophistiquées. Cette intégration relie votre automatisation de workflow et vos opérations informatiques, permettant à vos agents de gérer les demandes de service, les incidents, les utilisateurs et les actifs sans intervention manuelle. En connectant Sim avec ServiceNow, vous pouvez automatiser les tâches de gestion des services, améliorer les temps de réponse et garantir un accès cohérent et sécurisé aux données de service vitales de votre organisation.
{/* MANUAL-CONTENT-END */}
## Instructions d'utilisation
Intégrez ServiceNow dans votre workflow. Créez, lisez, mettez à jour et supprimez des enregistrements dans n'importe quelle table ServiceNow, y compris les incidents, les tâches, les demandes de changement, les utilisateurs et bien plus encore.
## Outils
### `servicenow_create_record`
Créer un nouvel enregistrement dans une table ServiceNow
#### Entrée
| Paramètre | Type | Requis | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Oui | URL de l'instance ServiceNow (par ex., https://instance.service-now.com) |
| `username` | string | Oui | Nom d'utilisateur ServiceNow |
| `password` | string | Oui | Mot de passe ServiceNow |
| `tableName` | string | Oui | Nom de la table (par ex., incident, task, sys_user) |
| `fields` | json | Oui | Champs à définir sur l'enregistrement (objet JSON) |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `record` | json | Enregistrement ServiceNow créé avec sys_id et autres champs |
| `metadata` | json | Métadonnées de l'opération |
### `servicenow_read_record`
Lire des enregistrements d'une table ServiceNow
#### Entrée
| Paramètre | Type | Requis | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Oui | URL de l'instance ServiceNow (par ex., https://instance.service-now.com) |
| `username` | string | Oui | Nom d'utilisateur ServiceNow |
| `password` | string | Oui | Mot de passe ServiceNow |
| `tableName` | string | Oui | Nom de la table |
| `sysId` | string | Non | sys_id d'enregistrement spécifique |
| `number` | string | Non | Numéro d'enregistrement (par ex., INC0010001) |
| `query` | string | Non | Chaîne de requête encodée (par ex., "active=true^priority=1") |
| `limit` | number | Non | Nombre maximum d'enregistrements à retourner |
| `fields` | string | Non | Liste de champs à retourner, séparés par des virgules |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `records` | array | Tableau d'enregistrements ServiceNow |
| `metadata` | json | Métadonnées de l'opération |
### `servicenow_update_record`
Mettre à jour un enregistrement existant dans une table ServiceNow
#### Entrée
| Paramètre | Type | Requis | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Oui | URL de l'instance ServiceNow \(par exemple, https://instance.service-now.com\) |
| `username` | string | Oui | Nom d'utilisateur ServiceNow |
| `password` | string | Oui | Mot de passe ServiceNow |
| `tableName` | string | Oui | Nom de la table |
| `sysId` | string | Oui | sys_id de l'enregistrement à mettre à jour |
| `fields` | json | Oui | Champs à mettre à jour \(objet JSON\) |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `record` | json | Enregistrement ServiceNow mis à jour |
| `metadata` | json | Métadonnées de l'opération |
### `servicenow_delete_record`
Supprimer un enregistrement d'une table ServiceNow
#### Entrée
| Paramètre | Type | Requis | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Oui | URL de l'instance ServiceNow \(par exemple, https://instance.service-now.com\) |
| `username` | string | Oui | Nom d'utilisateur ServiceNow |
| `password` | string | Oui | Mot de passe ServiceNow |
| `tableName` | string | Oui | Nom de la table |
| `sysId` | string | Oui | sys_id de l'enregistrement à supprimer |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Indique si la suppression a réussi |
| `metadata` | json | Métadonnées de l'opération |
## Remarques
- Catégorie : `tools`
- Type : `servicenow`

View File

@@ -107,14 +107,14 @@ Lisez les derniers messages des canaux Slack. Récupérez l'historique des conve
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| --------- | ---- | ----------- | ----------- |
| `authMethod` | chaîne | Non | Méthode d'authentification : oauth ou bot_token |
| `botToken` | chaîne | Non | Jeton du bot pour Bot personnalisé |
| `channel` | chaîne | Non | Canal Slack pour lire les messages \(ex. : #general\) |
| `userId` | chaîne | Non | ID utilisateur pour la conversation en MP \(ex. : U1234567890\) |
| `limit` | nombre | Non | Nombre de messages à récupérer \(par défaut : 10, max : 100\) |
| `oldest` | chaîne | Non | Début de la plage temporelle \(horodatage\) |
| `latest` | chaîne | Non | Fin de la plage temporelle \(horodatage\) |
| `channel` | chaîne | Non | Canal Slack depuis lequel lire les messages \(ex. : #general\) |
| `userId` | chaîne | Non | ID utilisateur pour la conversation en message direct \(ex. : U1234567890\) |
| `limit` | nombre | Non | Nombre de messages à récupérer \(par défaut : 10, max : 15\) |
| `oldest` | chaîne | Non | Début de la plage horaire \(horodatage\) |
| `latest` | chaîne | Non | Fin de la plage horaire \(horodatage\) |
#### Sortie

View File

@@ -37,16 +37,18 @@ Envoyez une requête de complétion de chat à n'importe quel fournisseur de LLM
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `model` | chaîne | Oui | Le modèle à utiliser (ex. : gpt-4o, claude-sonnet-4-5, gemini-2.0-flash) |
| `systemPrompt` | chaîne | Non | Instruction système pour définir le comportement de l'assistant |
| `context` | chaîne | Oui | Le message utilisateur ou le contexte à envoyer au modèle |
| `apiKey` | chaîne | Non | Clé API pour le fournisseur (utilise la clé de plateforme si non fournie pour les modèles hébergés) |
| `temperature` | nombre | Non | Température pour la génération de réponse (0-2) |
| `maxTokens` | nombre | Non | Nombre maximum de tokens dans la réponse |
| `azureEndpoint` | chaîne | Non | URL du point de terminaison Azure OpenAI |
| `azureApiVersion` | chaîne | Non | Version de l'API Azure OpenAI |
| Paramètre | Type | Requis | Description |
| --------- | ---- | -------- | ----------- |
| `model` | string | Oui | Le modèle à utiliser \(par exemple, gpt-4o, claude-sonnet-4-5, gemini-2.0-flash\) |
| `systemPrompt` | string | Non | Prompt système pour définir le comportement de l'assistant |
| `context` | string | Oui | Le message utilisateur ou le contexte à envoyer au modèle |
| `apiKey` | string | Non | Clé API pour le fournisseur \(utilise la clé de la plateforme si non fournie pour les modèles hébergés\) |
| `temperature` | number | Non | Température pour la génération de réponse \(0-2\) |
| `maxTokens` | number | Non | Nombre maximum de tokens dans la réponse |
| `azureEndpoint` | string | Non | URL du point de terminaison Azure OpenAI |
| `azureApiVersion` | string | Non | Version de l'API Azure OpenAI |
| `vertexProject` | string | Non | ID du projet Google Cloud pour Vertex AI |
| `vertexLocation` | string | Non | Emplacement Google Cloud pour Vertex AI \(par défaut us-central1\) |
#### Sortie

View File

@@ -110,26 +110,24 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
```json
{
"content": "Original content passed through",
"conditionResult": true,
"selectedPath": {
"blockId": "2acd9007-27e8-4510-a487-73d3b825e7c1",
"blockType": "agent",
"blockTitle": "Follow-up Agent"
},
"selectedConditionId": "condition-1"
"selectedOption": "condition-1"
}
```
### 条件ブロックの出力フィールド
- **content**: そのまま渡される元のコンテンツ
- **conditionResult**: 条件評価の真偽値結果
- **selectedPath**: 選択されたパスに関する情報
- **blockId**: 選択されたパスの次のブロックのID
- **blockType**: 次のブロックのタイプ
- **blockTitle**: 次のブロックのタイトル
- **selectedConditionId**: 選択された条件のID
- **selectedOption**: 選択された条件のID
</Tab>
<Tab>

View File

@@ -94,10 +94,16 @@ Jira課題を作成する
| `projectId` | string | はい | 課題のプロジェクトID |
| `summary` | string | はい | 課題の要約 |
| `description` | string | いいえ | 課題の説明 |
| `priority` | string | いいえ | 課題の優先度 |
| `assignee` | string | いいえ | 課題の担当者 |
| `priority` | string | いいえ | 課題の優先度IDまたは名前「10000」または「高」 |
| `assignee` | string | いいえ | 課題の担当者アカウントID |
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID。提供されない場合、ドメインを使用して取得されます。 |
| `issueType` | string | はい | 作成する課題のタイプ(例:タスク、ストーリー) |
| `labels` | array | いいえ | 課題のラベル(ラベル名の配列) |
| `duedate` | string | いいえ | 課題の期限形式YYYY-MM-DD |
| `reporter` | string | いいえ | 課題の報告者アカウントID |
| `environment` | string | いいえ | 課題の環境情報 |
| `customFieldId` | string | いいえ | カスタムフィールドIDcustomfield_10001 |
| `customFieldValue` | string | いいえ | カスタムフィールドの値 |
#### 出力
@@ -106,7 +112,8 @@ Jira課題を作成する
| `ts` | string | 操作のタイムスタンプ |
| `issueKey` | string | 作成された課題キーPROJ-123 |
| `summary` | string | 課題の要約 |
| `url` | string | 作成された課題のURL |
| `url` | string | 作成された課題のURL |
| `assigneeId` | string | 割り当てられたユーザーのアカウントID割り当てられている場合 |
### `jira_bulk_read`
@@ -520,7 +527,31 @@ Jira課題からウォッチャーを削除する
| `issueKey` | string | 課題キー |
| `watcherAccountId` | string | 削除されたウォッチャーのアカウントID |
## 注意事項
### `jira_get_users`
- カテゴリー: `tools`
- タイプ: `jira`
Jiraユーザーを取得します。アカウントIDが提供された場合、単一のユーザーを返します。それ以外の場合、すべてのユーザーのリストを返します。
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `domain` | string | はい | あなたのJiraドメインyourcompany.atlassian.net |
| `accountId` | string | いいえ | 特定のユーザーを取得するためのオプションのアカウントID。提供されない場合、すべてのユーザーを返します。 |
| `startAt` | number | いいえ | 返す最初のユーザーのインデックスページネーション用、デフォルト0 |
| `maxResults` | number | いいえ | 返すユーザーの最大数デフォルト50 |
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID。提供されない場合、ドメインを使用して取得されます。 |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `ts` | string | 操作のタイムスタンプ |
| `users` | json | accountId、displayName、emailAddress、activeステータス、avatarUrlsを含むユーザーの配列 |
| `total` | number | 返されたユーザーの総数 |
| `startAt` | number | ページネーション開始インデックス |
| `maxResults` | number | ページあたりの最大結果数 |
## 注記
- カテゴリ:`tools`
- タイプ:`jira`

View File

@@ -0,0 +1,124 @@
---
title: ServiceNow
description: ServiceNowレコードの作成、読み取り、更新、削除
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="servicenow"
color="#032D42"
/>
{/* MANUAL-CONTENT-START:intro */}
[ServiceNow](https://www.servicenow.com/)は、組織全体のITサービス管理ITSM、ワークフロー、ビジネスプロセスを効率化し自動化するために設計された強力なクラウドプラットフォームです。ServiceNowを使用すると、広範なAPIを使用してインシデント、リクエスト、タスク、ユーザーなどを管理できます。
ServiceNowでは、次のことができます。
- **ITワークフローの自動化**: インシデント、タスク、変更リクエスト、ユーザーなど、任意のServiceNowテーブルのレコードを作成、読み取り、更新、削除します。
- **システムの統合**: ServiceNowを他のツールやプロセスと接続して、シームレスな自動化を実現します。
- **単一の信頼できる情報源の維持**: すべてのサービスおよび運用データを整理してアクセス可能な状態に保ちます。
- **運用効率の向上**: カスタマイズ可能なワークフローと自動化により、手作業を削減し、サービス品質を向上させます。
Simでは、ServiceNow統合により、エージェントがワークフローの一部としてServiceNowインスタンスと直接やり取りできるようになります。エージェントは、任意のServiceNowテーブルのレコードを作成、読み取り、更新、削除でき、チケットやユーザーデータを活用して高度な自動化と意思決定を行うことができます。この統合により、ワークフロー自動化とIT運用が橋渡しされ、エージェントは手動介入なしでサービスリクエスト、インシデント、ユーザー、資産を管理できるようになります。SimとServiceNowを接続することで、サービス管理タスクを自動化し、応答時間を改善し、組織の重要なサービスデータへの一貫性のある安全なアクセスを確保できます。
{/* MANUAL-CONTENT-END */}
## 使用方法
ServiceNowをワークフローに統合します。インシデント、タスク、変更リクエスト、ユーザーなど、任意のServiceNowテーブルのレコードを作成、読み取り、更新、削除します。
## ツール
### `servicenow_create_record`
ServiceNowテーブルに新しいレコードを作成
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | はい | ServiceNowインスタンスURL(例: https://instance.service-now.com) |
| `username` | string | はい | ServiceNowユーザー名 |
| `password` | string | はい | ServiceNowパスワード |
| `tableName` | string | はい | テーブル名(例: incident、task、sys_user) |
| `fields` | json | はい | レコードに設定するフィールド(JSONオブジェクト) |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `record` | json | sys_idおよびその他のフィールドを含む作成されたServiceNowレコード |
| `metadata` | json | 操作メタデータ |
### `servicenow_read_record`
ServiceNowテーブルからレコードを読み取ります
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | はい | ServiceNowインスタンスURL(例: https://instance.service-now.com) |
| `username` | string | はい | ServiceNowユーザー名 |
| `password` | string | はい | ServiceNowパスワード |
| `tableName` | string | はい | テーブル名 |
| `sysId` | string | いいえ | 特定のレコードのsys_id |
| `number` | string | いいえ | レコード番号(例: INC0010001) |
| `query` | string | いいえ | エンコードされたクエリ文字列(例: "active=true^priority=1") |
| `limit` | number | いいえ | 返すレコードの最大数 |
| `fields` | string | いいえ | 返すフィールドのカンマ区切りリスト |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `records` | array | ServiceNowレコードの配列 |
| `metadata` | json | 操作メタデータ |
### `servicenow_update_record`
ServiceNowテーブル内の既存のレコードを更新
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | はい | ServiceNowインスタンスURLhttps://instance.service-now.com |
| `username` | string | はい | ServiceNowユーザー名 |
| `password` | string | はい | ServiceNowパスワード |
| `tableName` | string | はい | テーブル名 |
| `sysId` | string | はい | 更新するレコードのsys_id |
| `fields` | json | はい | 更新するフィールドJSONオブジェクト |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `record` | json | 更新されたServiceNowレコード |
| `metadata` | json | 操作メタデータ |
### `servicenow_delete_record`
ServiceNowテーブルからレコードを削除
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | はい | ServiceNowインスタンスURLhttps://instance.service-now.com |
| `username` | string | はい | ServiceNowユーザー名 |
| `password` | string | はい | ServiceNowパスワード |
| `tableName` | string | はい | テーブル名 |
| `sysId` | string | はい | 削除するレコードのsys_id |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 削除が成功したかどうか |
| `metadata` | json | 操作メタデータ |
## 注意事項
- カテゴリ: `tools`
- タイプ: `servicenow`

View File

@@ -110,8 +110,8 @@ Slackチャンネルから最新のメッセージを読み取ります。フィ
| `authMethod` | string | いいえ | 認証方法oauthまたはbot_token |
| `botToken` | string | いいえ | カスタムボット用のボットトークン |
| `channel` | string | いいえ | メッセージを読み取るSlackチャンネル#general |
| `userId` | string | いいえ | DM会話用のユーザーIDU1234567890 |
| `limit` | number | いいえ | 取得するメッセージ数デフォルト10、最大100 |
| `userId` | string | いいえ | DM会話用のユーザーIDU1234567890 |
| `limit` | number | いいえ | 取得するメッセージ数デフォルト10、最大15 |
| `oldest` | string | いいえ | 時間範囲の開始(タイムスタンプ) |
| `latest` | string | いいえ | 時間範囲の終了(タイムスタンプ) |

View File

@@ -42,11 +42,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| `model` | string | はい | 使用するモデルgpt-4o、claude-sonnet-4-5、gemini-2.0-flash |
| `systemPrompt` | string | いいえ | アシスタントの動作を設定するシステムプロンプト |
| `context` | string | はい | モデルに送信するユーザーメッセージまたはコンテキスト |
| `apiKey` | string | いいえ | プロバイダーのAPIキーホストされたモデルの場合、提供されなければプラットフォームキーを使用) |
| `apiKey` | string | いいえ | プロバイダーのAPIキーホストされたモデルの場合、提供されない場合はプラットフォームキーを使用) |
| `temperature` | number | いいえ | レスポンス生成の温度0-2 |
| `maxTokens` | number | いいえ | レスポンスの最大トークン数 |
| `azureEndpoint` | string | いいえ | Azure OpenAIエンドポイントURL |
| `azureApiVersion` | string | いいえ | Azure OpenAI APIバージョン |
| `vertexProject` | string | いいえ | Vertex AI用のGoogle CloudプロジェクトID |
| `vertexLocation` | string | いいえ | Vertex AI用のGoogle Cloudロケーションデフォルトはus-central1 |
#### 出力

View File

@@ -110,26 +110,24 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
```json
{
"content": "Original content passed through",
"conditionResult": true,
"selectedPath": {
"blockId": "2acd9007-27e8-4510-a487-73d3b825e7c1",
"blockType": "agent",
"blockTitle": "Follow-up Agent"
},
"selectedConditionId": "condition-1"
"selectedOption": "condition-1"
}
```
### 条件模块输出字段
- **content**:传递的原始内容
- **conditionResult**:条件评估的布尔结果
- **selectedPath**:关于选定路径的信息
- **blockId**:选定路径中下一个块的 ID
- **blockType**:下一个块的类型
- **blockTitle**:下一个模块的标题
- **selectedConditionId**:选定条件的 ID
- **conditionResult**:条件判断的布尔值结果
- **selectedPath**:所选路径的信息
- **blockId**:所选路径下一个区块的 ID
- **blockType**下一个块的类型
- **blockTitle**:下一个块的标题
- **selectedOption**:所选条件的 ID
</Tab>
<Tab>

View File

@@ -91,13 +91,19 @@ Jira 的主要功能包括:
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `domain` | 字符串 | 是 | 您的 Jira 域名 \(例如yourcompany.atlassian.net\) |
| `projectId` | 字符串 | 是 | 问题项目 ID |
| `summary` | 字符串 | 是 | 问题摘要 |
| `description` | 字符串 | 否 | 问题描述 |
| `priority` | 字符串 | 否 | 问题优先级 |
| `assignee` | 字符串 | 否 | 问题负责人 |
| `cloudId` | 字符串 | 否 | 实例的 Jira ID。如果未提供将使用域名获取。 |
| `issueType` | 字符串 | 是 | 要创建的问题类型 \(例如:任务、故事\) |
| `projectId` | 字符串 | 是 | 问题所属项目 ID |
| `summary` | 字符串 | 是 | 问题摘要 |
| `description` | 字符串 | 否 | 问题描述 |
| `priority` | 字符串 | 否 | 问题优先级 ID 或名称 \(例如“10000”或“High”\) |
| `assignee` | 字符串 | 否 | 问题负责人账户 ID |
| `cloudId` | 字符串 | 否 | 实例的 Jira Cloud ID。如果未提供将使用域名获取。 |
| `issueType` | 字符串 | 是 | 要创建的问题类型 \(例如:Task、Story\) |
| `labels` | 数组 | 否 | 问题标签 \(标签名称数组\) |
| `duedate` | 字符串 | 否 | 问题截止日期 \(格式YYYY-MM-DD\) |
| `reporter` | 字符串 | 否 | 问题报告人账户 ID |
| `environment` | 字符串 | 否 | 问题环境信息 |
| `customFieldId` | 字符串 | 否 | 自定义字段 ID \(例如customfield_10001\) |
| `customFieldValue` | 字符串 | 否 | 自定义字段的值 |
#### 输出
@@ -107,6 +113,7 @@ Jira 的主要功能包括:
| `issueKey` | 字符串 | 创建的问题键 \(例如PROJ-123\) |
| `summary` | 字符串 | 问题摘要 |
| `url` | 字符串 | 创建的问题的 URL |
| `assigneeId` | 字符串 | 已分配用户的账户 ID如已分配 |
### `jira_bulk_read`
@@ -520,7 +527,31 @@ Jira 的主要功能包括:
| `issueKey` | string | 问题键 |
| `watcherAccountId` | string | 移除的观察者账户 ID |
## 注意事项
### `jira_get_users`
- 类别: `tools`
- 类型: `jira`
获取 Jira 用户。如果提供了账户 ID则返回单个用户否则返回所有用户的列表。
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `domain` | 字符串 | 是 | 您的 Jira 域名 \(例如yourcompany.atlassian.net\) |
| `accountId` | 字符串 | 否 | 可选账户 ID用于获取特定用户。如果未提供则返回所有用户。 |
| `startAt` | 数字 | 否 | 要返回的第一个用户的索引 \(用于分页默认值0\) |
| `maxResults` | 数字 | 否 | 要返回的最大用户数 \(默认值50\) |
| `cloudId` | 字符串 | 否 | 实例的 Jira Cloud ID。如果未提供将使用域名获取。 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `ts` | 字符串 | 操作的时间戳 |
| `users` | json | 用户数组,包含 accountId、displayName、emailAddress、active 状态和 avatarUrls |
| `total` | 数字 | 返回的用户总数 |
| `startAt` | 数字 | 分页起始索引 |
| `maxResults` | 数字 | 每页最大结果数 |
## 备注
- 分类:`tools`
- 类型:`jira`

View File

@@ -0,0 +1,124 @@
---
title: ServiceNow
description: 创建、读取、更新和删除 ServiceNow 记录
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="servicenow"
color="#032D42"
/>
{/* MANUAL-CONTENT-START:intro */}
[ServiceNow](https://www.servicenow.com/) 是一款强大的云平台,旨在简化和自动化 IT 服务管理ITSM、工作流以及企业各类业务流程。ServiceNow 让您能够通过其强大的 API 管理事件、请求、任务、用户等多种内容。
使用 ServiceNow您可以
- **自动化 IT 工作流**:在任意 ServiceNow 表中创建、读取、更新和删除记录,如事件、任务、变更请求和用户等。
- **集成系统**:将 ServiceNow 与您的其他工具和流程连接,实现无缝自动化。
- **维护单一数据源**:让所有服务和运营数据井然有序,便于访问。
- **提升运营效率**:通过可定制的工作流和自动化,减少手动操作,提高服务质量。
在 Sim 中ServiceNow 集成让您的代理能够在工作流中直接与 ServiceNow 实例交互。代理可以在任意 ServiceNow 表中创建、读取、更新或删除记录,并利用工单或用户数据实现复杂的自动化和决策。这一集成将您的工作流自动化与 IT 运维无缝衔接,使代理能够自动化管理服务请求、事件、用户和资产,无需人工干预。通过将 Sim 与 ServiceNow 连接,您可以自动化服务管理任务、提升响应速度,并确保对组织关键服务数据的持续、安全访问。
{/* MANUAL-CONTENT-END */}
## 使用说明
将 ServiceNow 集成到您的工作流中。在任意 ServiceNow 表(包括事件、任务、变更请求、用户等)中创建、读取、更新和删除记录。
## 工具
### `servicenow_create_record`
在 ServiceNow 表中创建新记录
#### 输入
| 参数 | 类型 | 是否必填 | 描述 |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | 是 | ServiceNow 实例 URL例如https://instance.service-now.com |
| `username` | string | 是 | ServiceNow 用户名 |
| `password` | string | 是 | ServiceNow 密码 |
| `tableName` | string | 是 | 表名例如incident、task、sys_user |
| `fields` | json | 是 | 记录中要设置的字段JSON 对象) |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `record` | json | 创建的 ServiceNow 记录,包含 sys_id 及其他字段 |
| `metadata` | json | 操作元数据 |
### `servicenow_read_record`
从 ServiceNow 表中读取记录
#### 输入
| 参数 | 类型 | 是否必填 | 描述 |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | 是 | ServiceNow 实例 URL例如https://instance.service-now.com |
| `username` | string | 是 | ServiceNow 用户名 |
| `password` | string | 是 | ServiceNow 密码 |
| `tableName` | string | 是 | 表名 |
| `sysId` | string | 否 | 指定记录 sys_id |
| `number` | string | 否 | 记录编号例如INC0010001 |
| `query` | string | 否 | 编码查询字符串(例如:"active=true^priority=1" |
| `limit` | number | 否 | 返回的最大记录数 |
| `fields` | string | 否 | 要返回的字段列表(以逗号分隔) |
#### 输出
| 参数 | 类型 | 说明 |
| --------- | ---- | ----------- |
| `records` | array | ServiceNow 记录数组 |
| `metadata` | json | 操作元数据 |
### `servicenow_update_record`
更新 ServiceNow 表中的现有记录
#### 输入
| 参数 | 类型 | 必填 | 说明 |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | 是 | ServiceNow 实例 URL例如https://instance.service-now.com |
| `username` | string | 是 | ServiceNow 用户名 |
| `password` | string | 是 | ServiceNow 密码 |
| `tableName` | string | 是 | 表名 |
| `sysId` | string | 是 | 要更新的记录 sys_id |
| `fields` | json | 是 | 要更新的字段JSON 对象) |
#### 输出
| 参数 | 类型 | 说明 |
| --------- | ---- | ----------- |
| `record` | json | 已更新的 ServiceNow 记录 |
| `metadata` | json | 操作元数据 |
### `servicenow_delete_record`
从 ServiceNow 表中删除记录
#### 输入
| 参数 | 类型 | 必填 | 说明 |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | 是 | ServiceNow 实例 URL例如https://instance.service-now.com |
| `username` | string | 是 | ServiceNow 用户名 |
| `password` | string | 是 | ServiceNow 密码 |
| `tableName` | string | 是 | 表名 |
| `sysId` | string | 是 | 要删除的记录 sys_id |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 删除是否成功 |
| `metadata` | json | 操作元数据 |
## 备注
- 分类:`tools`
- 类型:`servicenow`

View File

@@ -109,10 +109,10 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| `authMethod` | string | 否 | 认证方法oauth 或 bot_token |
| `botToken` | string | 否 | 自定义 Bot 的令牌 |
| `channel` | string | 否 | 要读取消息的 Slack 频道(例如,#general |
| `userId` | string | 否 | DM 话的用户 ID例如U1234567890 |
| `limit` | number | 否 | 要检索的消息数量默认10最大100 |
| `oldest` | string | 否 | 时间范围的开始(时间戳) |
| `latest` | string | 否 | 时间范围结束(时间戳) |
| `userId` | string | 否 | DM 话的用户 ID例如U1234567890 |
| `limit` | number | 否 | 要检索的消息数量默认10最大15 |
| `oldest` | string | 否 | 时间范围始(时间戳) |
| `latest` | string | 否 | 时间范围结束(时间戳) |
#### 输出

View File

@@ -37,16 +37,18 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
#### 输入
| 参数 | 类型 | 必 | 描述 |
| 参数 | 类型 | 必 | 说明 |
| --------- | ---- | -------- | ----------- |
| `model` | string | 是 | 要使用的模型 \(例如gpt-4o、claude-sonnet-4-5、gemini-2.0-flash\) |
| `systemPrompt` | string | 否 | 设置助手行为的系统提示 |
| `context` | string | 是 | 发送给模型的用户消息或上下文 |
| `apiKey` | string | 否 | 提供的 API 密钥 \(如果未为托管模型提供,则使用平台密钥\) |
| `temperature` | number | 否 | 响应生成的温度 \(0-2\) |
| `maxTokens` | number | 否 | 响应的最大令牌数 |
| `azureEndpoint` | string | 否 | Azure OpenAI 端点 URL |
| `model` | string | 是 | 要使用的模型例如 gpt-4o、claude-sonnet-4-5、gemini-2.0-flash |
| `systemPrompt` | string | 否 | 设置助手行为的 system prompt |
| `context` | string | 是 | 发送给模型的用户消息或上下文 |
| `apiKey` | string | 否 | 提供的 API key如未提供托管模型将使用平台密钥 |
| `temperature` | number | 否 | 响应生成的 temperature0-2 |
| `maxTokens` | number | 否 | 响应的最大 tokens 数 |
| `azureEndpoint` | string | 否 | Azure OpenAI endpoint URL |
| `azureApiVersion` | string | 否 | Azure OpenAI API 版本 |
| `vertexProject` | string | 否 | Vertex AI 的 Google Cloud 项目 ID |
| `vertexLocation` | string | 否 | Vertex AI 的 Google Cloud 区域(默认为 us-central1 |
#### 输出

View File

@@ -557,7 +557,7 @@ checksums:
content/8: 6325adefb6e1520835225285b18b6a45
content/9: b7fa85fce9c7476fe132df189e27dac1
content/10: 371d0e46b4bd2c23f559b8bc112f6955
content/11: 985f435f721b00df4d13fa0a5552684c
content/11: 7ad14ccfe548588081626cfe769ad492
content/12: bcadfc362b69078beee0088e5936c98b
content/13: 6af66efd0da20944a87fdb8d9defa358
content/14: b3f310d5ef115bea5a8b75bf25d7ea9a
@@ -903,7 +903,7 @@ checksums:
content/24: 228a8ece96627883153b826a1cbaa06c
content/25: 53abe061a259c296c82676b4770ddd1b
content/26: 371d0e46b4bd2c23f559b8bc112f6955
content/27: 03e8b10ec08b354de98e360b66b779e3
content/27: 5b9546f77fbafc0741f3fc2548f81c7e
content/28: bcadfc362b69078beee0088e5936c98b
content/29: b82def7d82657f941fbe60df3924eeeb
content/30: 1ca7ee3856805fa1718031c5f75b6ffb
@@ -2521,9 +2521,9 @@ checksums:
content/22: ef92d95455e378abe4d27a1cdc5e1aed
content/23: febd6019055f3754953fd93395d0dbf2
content/24: 371d0e46b4bd2c23f559b8bc112f6955
content/25: 7ef3f388e5ee9346bac54c771d825f40
content/25: caf6acbe2a4495ca055cb9006ce47250
content/26: bcadfc362b69078beee0088e5936c98b
content/27: e0fa91c45aa780fc03e91df77417f893
content/27: 57662dd91f8d1d807377fd48fa0e9142
content/28: b463f54cd5fe2458b5842549fbb5e1ce
content/29: 55f8c724e1a2463bc29a32518a512c73
content/30: 371d0e46b4bd2c23f559b8bc112f6955
@@ -2638,8 +2638,14 @@ checksums:
content/139: 33fde4c3da4584b51f06183b7b192a78
content/140: bcadfc362b69078beee0088e5936c98b
content/141: b7451190f100388d999c183958d787a7
content/142: b3f310d5ef115bea5a8b75bf25d7ea9a
content/143: 4930918f803340baa861bed9cdf789de
content/142: d0f9e799e2e5cc62de60668d35fd846f
content/143: b19069ff19899fe202217e06e002c447
content/144: 371d0e46b4bd2c23f559b8bc112f6955
content/145: 480fd62f8d9cc18467e82f4c3f70beea
content/146: bcadfc362b69078beee0088e5936c98b
content/147: 4e73a65d3b873f3979587e10a0f39e72
content/148: b3f310d5ef115bea5a8b75bf25d7ea9a
content/149: 4930918f803340baa861bed9cdf789de
8f76e389f6226f608571622b015ca6a1:
meta/title: ddfe2191ea61b34d8b7cc1d7c19b94ac
meta/description: 049ff551f2ebabb15cdea0c71bd8e4eb
@@ -4811,9 +4817,9 @@ checksums:
content/19: 85547efea8ae0e8170ac4e2030f6be25
content/20: 25c56dcdc4af1516c3fbf9d82d96b48d
content/21: 56dbe63da14a319cd520ab1615c94be7
content/22: e092cde0c92ef09c642a62636e7e3ae3
content/22: e039f6c905c8aa148cc3e7af19f05239
content/23: c7004f5db8f7134d7e3a36a1916691a2
content/24: bbc26961050b132b9bc4f14ba11f407a
content/24: 26555018b90fc8fb3ac65cece15f3966
content/25: 56dbe63da14a319cd520ab1615c94be7
content/26: 3e835ecc38acf2c76179034360d41670
content/27: a13bbc3dac7388e1ef4e9cbafdcc8241
@@ -49822,3 +49828,41 @@ checksums:
content/472: dbc5fceeefb3ab5fa505394becafef4e
content/473: b3f310d5ef115bea5a8b75bf25d7ea9a
content/474: 27c398e669b297cea076e4ce4cc0c5eb
9a28da736b42bf8de55126d4c06b6150:
meta/title: 418d5c8a18ad73520b38765741601f32
meta/description: 41cb31abf94297849fb8a4023cf0211d
content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
content/1: e72670f88454b5b1c955b029de5fa8b5
content/2: d586e5af506d99add847369c0accfb4d
content/3: a2ce9ed4954ab55bcebed927cec8e890
content/4: 5fc7b723a6adcf201e8deb3f5ed9a9e3
content/5: a78981875c359a3343f26ed4d115f899
content/6: 821e6394b0a953e2b0842b04ae8f3105
content/7: 56a538eaccb1158fb1f7a01cc32f7331
content/8: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
content/9: 263633aee6db9332de806ae50d87de05
content/10: 5a7e2171e5f73fec5eae21a50e5de661
content/11: 371d0e46b4bd2c23f559b8bc112f6955
content/12: 5905ef5d0db0354c08394acb0b5cda4b
content/13: bcadfc362b69078beee0088e5936c98b
content/14: d81ef802f80143282cf4e534561a9570
content/15: 02233e6212003c1d121424cfd8b86b62
content/16: efe2c6dd368708de68a1addbfdb11b0c
content/17: 371d0e46b4bd2c23f559b8bc112f6955
content/18: 2722e8bee100e7bc4590fa02710e9508
content/19: bcadfc362b69078beee0088e5936c98b
content/20: 953f353184dc27db1f20156db2a9ad90
content/21: 2011e87d0555cd0ab133ef2d35e7a37b
content/22: dbf08acb413d845ec419e45b1f986bdb
content/23: 371d0e46b4bd2c23f559b8bc112f6955
content/24: afc35de2990ed0e9bb8f98dc1b9609ce
content/25: bcadfc362b69078beee0088e5936c98b
content/26: c06a5bb458242baa23d34957034c2fe7
content/27: ff043e912417bc29ac7c64520160c07d
content/28: 9c2175ab469cb6ff9e62bc8bdcf7621d
content/29: 371d0e46b4bd2c23f559b8bc112f6955
content/30: 20e6bddad8e7f34a3d09e5b0c5678c13
content/31: bcadfc362b69078beee0088e5936c98b
content/32: fd0f38eb3fe5cf95be366a4ff6b4fb90
content/33: b3f310d5ef115bea5a8b75bf25d7ea9a
content/34: 4a7b2c644e487f3d12b6a6b54f8c6773

View File

@@ -573,10 +573,10 @@ export default function LoginPage({
<Dialog open={forgotPasswordOpen} onOpenChange={setForgotPasswordOpen}>
<DialogContent className='auth-card auth-card-shadow max-w-[540px] rounded-[10px] border backdrop-blur-sm'>
<DialogHeader>
<DialogTitle className='auth-text-primary font-semibold text-xl tracking-tight'>
<DialogTitle className='font-semibold text-black text-xl tracking-tight'>
Reset Password
</DialogTitle>
<DialogDescription className='auth-text-secondary text-sm'>
<DialogDescription className='text-muted-foreground text-sm'>
Enter your email address and we'll send you a link to reset your password if your
account exists.
</DialogDescription>

View File

@@ -70,6 +70,7 @@ export const FOOTER_TOOLS = [
'Salesforce',
'SendGrid',
'Serper',
'ServiceNow',
'SharePoint',
'Slack',
'Smtp',

View File

@@ -109,7 +109,7 @@ export default function Footer({ fullWidth = false }: FooterProps) {
{FOOTER_BLOCKS.map((block) => (
<Link
key={block}
href={`https://docs.sim.ai/blocks/${block.toLowerCase().replace(' ', '-')}`}
href={`https://docs.sim.ai/blocks/${block.toLowerCase().replaceAll(' ', '-')}`}
target='_blank'
rel='noopener noreferrer'
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'

View File

@@ -2,7 +2,6 @@ import { Suspense } from 'react'
import dynamic from 'next/dynamic'
import { Background, Footer, Nav, StructuredData } from '@/app/(landing)/components'
// Lazy load heavy components for better initial load performance
const Hero = dynamic(() => import('@/app/(landing)/components/hero/hero'), {
loading: () => <div className='h-[600px] animate-pulse bg-gray-50' />,
})

View File

@@ -1,8 +1,7 @@
import Image from 'next/image'
import Link from 'next/link'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { getAllPostMeta } from '@/lib/blog/registry'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { PostGrid } from '@/app/(landing)/studio/post-grid'
export const revalidate = 3600
@@ -18,7 +17,6 @@ export default async function StudioIndex({
const all = await getAllPostMeta()
const filtered = tag ? all.filter((p) => p.tags.includes(tag)) : all
// Sort to ensure featured post is first on page 1
const sorted =
pageNum === 1
? filtered.sort((a, b) => {
@@ -63,69 +61,7 @@ export default async function StudioIndex({
</div> */}
{/* Grid layout for consistent rows */}
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
{posts.map((p, i) => {
return (
<Link key={p.slug} href={`/studio/${p.slug}`} className='group flex flex-col'>
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 transition-colors duration-300 hover:border-gray-300'>
<Image
src={p.ogImage}
alt={p.title}
width={800}
height={450}
className='h-48 w-full object-cover'
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
loading='lazy'
unoptimized
/>
<div className='flex flex-1 flex-col p-4'>
<div className='mb-2 text-gray-600 text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<h3 className='shine-text mb-1 font-medium text-lg leading-tight'>{p.title}</h3>
<p className='mb-3 line-clamp-3 flex-1 text-gray-700 text-sm'>{p.description}</p>
<div className='flex items-center gap-2'>
<div className='-space-x-1.5 flex'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 3)
.map((author, idx) => (
<Avatar key={idx} className='size-4 border border-white'>
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
<AvatarFallback className='border border-white bg-gray-100 text-[10px] text-gray-600'>
{author?.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
))}
</div>
<span className='text-gray-600 text-xs'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 2)
.map((a) => a?.name)
.join(', ')}
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length > 2 && (
<>
{' '}
and{' '}
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2}{' '}
other
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2 >
1
? 's'
: ''}
</>
)}
</span>
</div>
</div>
</div>
</Link>
)
})}
</div>
<PostGrid posts={posts} />
{totalPages > 1 && (
<div className='mt-10 flex items-center justify-center gap-3'>

View File

@@ -0,0 +1,90 @@
'use client'
import Image from 'next/image'
import Link from 'next/link'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
interface Author {
id: string
name: string
avatarUrl?: string
url?: string
}
interface Post {
slug: string
title: string
description: string
date: string
ogImage: string
author: Author
authors?: Author[]
featured?: boolean
}
export function PostGrid({ posts }: { posts: Post[] }) {
return (
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
{posts.map((p, index) => (
<Link key={p.slug} href={`/studio/${p.slug}`} className='group flex flex-col'>
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 transition-colors duration-300 hover:border-gray-300'>
{/* Image container with fixed aspect ratio to prevent layout shift */}
<div className='relative aspect-video w-full overflow-hidden'>
<Image
src={p.ogImage}
alt={p.title}
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
unoptimized
priority={index < 6}
loading={index < 6 ? undefined : 'lazy'}
fill
style={{ objectFit: 'cover' }}
/>
</div>
<div className='flex flex-1 flex-col p-4'>
<div className='mb-2 text-gray-600 text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<h3 className='shine-text mb-1 font-medium text-lg leading-tight'>{p.title}</h3>
<p className='mb-3 line-clamp-3 flex-1 text-gray-700 text-sm'>{p.description}</p>
<div className='flex items-center gap-2'>
<div className='-space-x-1.5 flex'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 3)
.map((author, idx) => (
<Avatar key={idx} className='size-4 border border-white'>
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
<AvatarFallback className='border border-white bg-gray-100 text-[10px] text-gray-600'>
{author?.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
))}
</div>
<span className='text-gray-600 text-xs'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 2)
.map((a) => a?.name)
.join(', ')}
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length > 2 && (
<>
{' '}
and {(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2}{' '}
other
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2 > 1
? 's'
: ''}
</>
)}
</span>
</div>
</div>
</div>
</Link>
))}
</div>
)
}

View File

@@ -12,6 +12,7 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
pathname === '/' ||
pathname.startsWith('/login') ||
pathname.startsWith('/signup') ||
pathname.startsWith('/reset-password') ||
pathname.startsWith('/sso') ||
pathname.startsWith('/terms') ||
pathname.startsWith('/privacy') ||

View File

@@ -759,3 +759,24 @@ input[type="search"]::-ms-clear {
--surface-elevated: #202020;
}
}
/**
* Remove backticks from inline code in prose (Tailwind Typography default)
*/
.prose code::before,
.prose code::after {
content: none !important;
}
/**
* Remove underlines from heading anchor links in prose
*/
.prose h1 a,
.prose h2 a,
.prose h3 a,
.prose h4 a,
.prose h5 a,
.prose h6 a {
text-decoration: none !important;
color: inherit !important;
}

View File

@@ -32,7 +32,17 @@ export async function GET(request: NextRequest) {
.from(account)
.where(and(...whereConditions))
return NextResponse.json({ accounts })
// Use the user's email as the display name (consistent with credential selector)
const userEmail = session.user.email
const accountsWithDisplayName = accounts.map((acc) => ({
id: acc.id,
accountId: acc.accountId,
providerId: acc.providerId,
displayName: userEmail || acc.providerId,
}))
return NextResponse.json({ accounts: accountsWithDisplayName })
} catch (error) {
logger.error('Failed to fetch accounts', { error })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })

View File

@@ -6,6 +6,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest, setupAuthApiMocks } from '@/app/api/__test-utils__/utils'
vi.mock('@/lib/core/utils/urls', () => ({
getBaseUrl: vi.fn(() => 'https://app.example.com'),
}))
describe('Forget Password API Route', () => {
beforeEach(() => {
vi.resetModules()
@@ -15,7 +19,7 @@ describe('Forget Password API Route', () => {
vi.clearAllMocks()
})
it('should send password reset email successfully', async () => {
it('should send password reset email successfully with same-origin redirectTo', async () => {
setupAuthApiMocks({
operations: {
forgetPassword: { success: true },
@@ -24,7 +28,7 @@ describe('Forget Password API Route', () => {
const req = createMockRequest('POST', {
email: 'test@example.com',
redirectTo: 'https://example.com/reset',
redirectTo: 'https://app.example.com/reset',
})
const { POST } = await import('@/app/api/auth/forget-password/route')
@@ -39,12 +43,36 @@ describe('Forget Password API Route', () => {
expect(auth.auth.api.forgetPassword).toHaveBeenCalledWith({
body: {
email: 'test@example.com',
redirectTo: 'https://example.com/reset',
redirectTo: 'https://app.example.com/reset',
},
method: 'POST',
})
})
it('should reject external redirectTo URL', async () => {
setupAuthApiMocks({
operations: {
forgetPassword: { success: true },
},
})
const req = createMockRequest('POST', {
email: 'test@example.com',
redirectTo: 'https://evil.com/phishing',
})
const { POST } = await import('@/app/api/auth/forget-password/route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.message).toBe('Redirect URL must be a valid same-origin URL')
const auth = await import('@/lib/auth')
expect(auth.auth.api.forgetPassword).not.toHaveBeenCalled()
})
it('should send password reset email without redirectTo', async () => {
setupAuthApiMocks({
operations: {

View File

@@ -1,6 +1,7 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { auth } from '@/lib/auth'
import { isSameOrigin } from '@/lib/core/utils/validation'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
@@ -13,10 +14,15 @@ const forgetPasswordSchema = z.object({
.email('Please provide a valid email address'),
redirectTo: z
.string()
.url('Redirect URL must be a valid URL')
.optional()
.or(z.literal(''))
.transform((val) => (val === '' ? undefined : val)),
.transform((val) => (val === '' || val === undefined ? undefined : val))
.refine(
(val) => val === undefined || (z.string().url().safeParse(val).success && isSameOrigin(val)),
{
message: 'Redirect URL must be a valid same-origin URL',
}
),
})
export async function POST(request: NextRequest) {

View File

@@ -38,7 +38,6 @@ vi.mock('@/lib/logs/console/logger', () => ({
}))
import { db } from '@sim/db'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshOAuthToken } from '@/lib/oauth/oauth'
import {
getCredential,
@@ -49,7 +48,6 @@ import {
const mockDb = db as any
const mockRefreshOAuthToken = refreshOAuthToken as any
const mockLogger = (createLogger as any)()
describe('OAuth Utils', () => {
beforeEach(() => {
@@ -87,7 +85,6 @@ describe('OAuth Utils', () => {
const userId = await getUserId('request-id')
expect(userId).toBeUndefined()
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should return undefined if workflow is not found', async () => {
@@ -96,7 +93,6 @@ describe('OAuth Utils', () => {
const userId = await getUserId('request-id', 'nonexistent-workflow-id')
expect(userId).toBeUndefined()
expect(mockLogger.warn).toHaveBeenCalled()
})
})
@@ -121,7 +117,6 @@ describe('OAuth Utils', () => {
const credential = await getCredential('request-id', 'nonexistent-id', 'test-user-id')
expect(credential).toBeUndefined()
expect(mockLogger.warn).toHaveBeenCalled()
})
})
@@ -139,7 +134,6 @@ describe('OAuth Utils', () => {
expect(mockRefreshOAuthToken).not.toHaveBeenCalled()
expect(result).toEqual({ accessToken: 'valid-token', refreshed: false })
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Access token is valid'))
})
it('should refresh token when expired', async () => {
@@ -163,9 +157,6 @@ describe('OAuth Utils', () => {
expect(mockDb.update).toHaveBeenCalled()
expect(mockDb.set).toHaveBeenCalled()
expect(result).toEqual({ accessToken: 'new-token', refreshed: true })
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringContaining('Successfully refreshed')
)
})
it('should handle refresh token error', async () => {
@@ -182,8 +173,6 @@ describe('OAuth Utils', () => {
await expect(
refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
).rejects.toThrow('Failed to refresh token')
expect(mockLogger.error).toHaveBeenCalled()
})
it('should not attempt refresh if no refresh token', async () => {
@@ -251,7 +240,6 @@ describe('OAuth Utils', () => {
const token = await refreshAccessTokenIfNeeded('nonexistent-id', 'test-user-id', 'request-id')
expect(token).toBeNull()
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should return null if refresh fails', async () => {
@@ -270,7 +258,6 @@ describe('OAuth Utils', () => {
const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
expect(token).toBeNull()
expect(mockLogger.error).toHaveBeenCalled()
})
})
})

View File

@@ -18,6 +18,7 @@ interface AccountInsertData {
updatedAt: Date
refreshToken?: string
idToken?: string
accessTokenExpiresAt?: Date
}
/**
@@ -103,6 +104,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
accessToken: account.accessToken,
refreshToken: account.refreshToken,
accessTokenExpiresAt: account.accessTokenExpiresAt,
idToken: account.idToken,
})
.from(account)
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))

View File

@@ -303,6 +303,14 @@ export async function POST(req: NextRequest) {
apiVersion: 'preview',
endpoint: env.AZURE_OPENAI_ENDPOINT,
}
} else if (providerEnv === 'vertex') {
providerConfig = {
provider: 'vertex',
model: modelToUse,
apiKey: env.COPILOT_API_KEY,
vertexProject: env.VERTEX_PROJECT,
vertexLocation: env.VERTEX_LOCATION,
}
} else {
providerConfig = {
provider: providerEnv,

View File

@@ -11,7 +11,7 @@ import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('CopilotChatsListAPI')
export async function GET(_req: NextRequest) {
export async function GET(_request: NextRequest) {
try {
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
if (!isAuthenticated || !userId) {

View File

@@ -66,6 +66,14 @@ export async function POST(req: NextRequest) {
apiVersion: env.AZURE_OPENAI_API_VERSION,
endpoint: env.AZURE_OPENAI_ENDPOINT,
}
} else if (providerEnv === 'vertex') {
providerConfig = {
provider: 'vertex',
model: modelToUse,
apiKey: env.COPILOT_API_KEY,
vertexProject: env.VERTEX_PROJECT,
vertexLocation: env.VERTEX_LOCATION,
}
} else {
providerConfig = {
provider: providerEnv,

View File

@@ -38,14 +38,13 @@ export async function GET(
const cloudKey = isCloudPath ? path.slice(1).join('/') : fullPath
const contextParam = request.nextUrl.searchParams.get('context')
const legacyBucketType = request.nextUrl.searchParams.get('bucket')
const context = contextParam || (isCloudPath ? inferContextFromKey(cloudKey) : undefined)
if (context === 'profile-pictures') {
logger.info('Serving public profile picture:', { cloudKey })
if (context === 'profile-pictures' || context === 'og-images') {
logger.info(`Serving public ${context}:`, { cloudKey })
if (isUsingCloudStorage() || isCloudPath) {
return await handleCloudProxyPublic(cloudKey, context, legacyBucketType)
return await handleCloudProxyPublic(cloudKey, context)
}
return await handleLocalFilePublic(fullPath)
}
@@ -182,8 +181,7 @@ async function handleCloudProxy(
async function handleCloudProxyPublic(
cloudKey: string,
context: StorageContext,
legacyBucketType?: string | null
context: StorageContext
): Promise<NextResponse> {
try {
let fileBuffer: Buffer

View File

@@ -1,7 +1,6 @@
import { runs } from '@trigger.dev/sdk'
import { type NextRequest, NextResponse } from 'next/server'
import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service'
import { getSession } from '@/lib/auth'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { createErrorResponse } from '@/app/api/workflows/utils'
@@ -18,38 +17,44 @@ export async function GET(
try {
logger.debug(`[${requestId}] Getting status for task: ${taskId}`)
// Try session auth first (for web UI)
const session = await getSession()
let authenticatedUserId: string | null = session?.user?.id || null
if (!authenticatedUserId) {
const apiKeyHeader = request.headers.get('x-api-key')
if (apiKeyHeader) {
const authResult = await authenticateApiKeyFromHeader(apiKeyHeader)
if (authResult.success && authResult.userId) {
authenticatedUserId = authResult.userId
if (authResult.keyId) {
await updateApiKeyLastUsed(authResult.keyId).catch((error) => {
logger.warn(`[${requestId}] Failed to update API key last used timestamp:`, {
keyId: authResult.keyId,
error,
})
})
}
}
}
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized task status request`)
return createErrorResponse(authResult.error || 'Authentication required', 401)
}
if (!authenticatedUserId) {
return createErrorResponse('Authentication required', 401)
}
const authenticatedUserId = authResult.userId
// Fetch task status from Trigger.dev
const run = await runs.retrieve(taskId)
logger.debug(`[${requestId}] Task ${taskId} status: ${run.status}`)
// Map Trigger.dev status to our format
const payload = run.payload as any
if (payload?.workflowId) {
const { verifyWorkflowAccess } = await import('@/socket-server/middleware/permissions')
const accessCheck = await verifyWorkflowAccess(authenticatedUserId, payload.workflowId)
if (!accessCheck.hasAccess) {
logger.warn(`[${requestId}] User ${authenticatedUserId} denied access to task ${taskId}`, {
workflowId: payload.workflowId,
})
return createErrorResponse('Access denied', 403)
}
logger.debug(`[${requestId}] User ${authenticatedUserId} has access to task ${taskId}`)
} else {
if (payload?.userId && payload.userId !== authenticatedUserId) {
logger.warn(
`[${requestId}] User ${authenticatedUserId} attempted to access task ${taskId} owned by ${payload.userId}`
)
return createErrorResponse('Access denied', 403)
}
if (!payload?.userId) {
logger.warn(
`[${requestId}] Task ${taskId} has no ownership information in payload. Denying access for security.`
)
return createErrorResponse('Access denied', 403)
}
}
const statusMap = {
QUEUED: 'queued',
WAITING_FOR_DEPLOY: 'queued',
@@ -67,7 +72,6 @@ export async function GET(
const mappedStatus = statusMap[run.status as keyof typeof statusMap] || 'unknown'
// Build response based on status
const response: any = {
success: true,
taskId,
@@ -77,21 +81,18 @@ export async function GET(
},
}
// Add completion details if finished
if (mappedStatus === 'completed') {
response.output = run.output // This contains the workflow execution results
response.metadata.completedAt = run.finishedAt
response.metadata.duration = run.durationMs
}
// Add error details if failed
if (mappedStatus === 'failed') {
response.error = run.error
response.metadata.completedAt = run.finishedAt
response.metadata.duration = run.durationMs
}
// Add progress info if still processing
if (mappedStatus === 'processing' || mappedStatus === 'queued') {
response.estimatedDuration = 180000 // 3 minutes max from our config
}
@@ -107,6 +108,3 @@ export async function GET(
return createErrorResponse('Failed to fetch task status', 500)
}
}
// TODO: Implement task cancellation via Trigger.dev API if needed
// export async function DELETE() { ... }

View File

@@ -27,7 +27,7 @@ const UpdateKnowledgeBaseSchema = z.object({
.optional(),
})
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params
@@ -133,7 +133,10 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
}
}
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = generateRequestId()
const { id } = await params

View File

@@ -1,32 +1,72 @@
import { db } from '@sim/db'
import { workflowExecutionLogs, workflowExecutionSnapshots } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import {
permissions,
workflow,
workflowExecutionLogs,
workflowExecutionSnapshots,
} from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('LogsByExecutionIdAPI')
export async function GET(
_request: NextRequest,
request: NextRequest,
{ params }: { params: Promise<{ executionId: string }> }
) {
const requestId = generateRequestId()
try {
const { executionId } = await params
logger.debug(`Fetching execution data for: ${executionId}`)
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized execution data access attempt for: ${executionId}`)
return NextResponse.json(
{ error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const authenticatedUserId = authResult.userId
logger.debug(
`[${requestId}] Fetching execution data for: ${executionId} (auth: ${authResult.authType})`
)
// Get the workflow execution log to find the snapshot
const [workflowLog] = await db
.select()
.select({
id: workflowExecutionLogs.id,
workflowId: workflowExecutionLogs.workflowId,
executionId: workflowExecutionLogs.executionId,
stateSnapshotId: workflowExecutionLogs.stateSnapshotId,
trigger: workflowExecutionLogs.trigger,
startedAt: workflowExecutionLogs.startedAt,
endedAt: workflowExecutionLogs.endedAt,
totalDurationMs: workflowExecutionLogs.totalDurationMs,
cost: workflowExecutionLogs.cost,
})
.from(workflowExecutionLogs)
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
.innerJoin(
permissions,
and(
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workflow.workspaceId),
eq(permissions.userId, authenticatedUserId)
)
)
.where(eq(workflowExecutionLogs.executionId, executionId))
.limit(1)
if (!workflowLog) {
logger.warn(`[${requestId}] Execution not found or access denied: ${executionId}`)
return NextResponse.json({ error: 'Workflow execution not found' }, { status: 404 })
}
// Get the workflow state snapshot
const [snapshot] = await db
.select()
.from(workflowExecutionSnapshots)
@@ -34,6 +74,7 @@ export async function GET(
.limit(1)
if (!snapshot) {
logger.warn(`[${requestId}] Workflow state snapshot not found for execution: ${executionId}`)
return NextResponse.json({ error: 'Workflow state snapshot not found' }, { status: 404 })
}
@@ -50,14 +91,14 @@ export async function GET(
},
}
logger.debug(`Successfully fetched execution data for: ${executionId}`)
logger.debug(`[${requestId}] Successfully fetched execution data for: ${executionId}`)
logger.debug(
`Workflow state contains ${Object.keys((snapshot.stateData as any)?.blocks || {}).length} blocks`
`[${requestId}] Workflow state contains ${Object.keys((snapshot.stateData as any)?.blocks || {}).length} blocks`
)
return NextResponse.json(response)
} catch (error) {
logger.error('Error fetching execution data:', error)
logger.error(`[${requestId}] Error fetching execution data:`, error)
return NextResponse.json({ error: 'Failed to fetch execution data' }, { status: 500 })
}
}

View File

@@ -5,6 +5,7 @@ import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
import type { McpServerStatusConfig } from '@/lib/mcp/types'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
const logger = createLogger('McpServerRefreshAPI')
@@ -50,6 +51,12 @@ export const POST = withMcpAuth<{ id: string }>('read')(
let toolCount = 0
let lastError: string | null = null
const currentStatusConfig: McpServerStatusConfig =
(server.statusConfig as McpServerStatusConfig | null) ?? {
consecutiveFailures: 0,
lastSuccessfulDiscovery: null,
}
try {
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
connectionStatus = 'connected'
@@ -63,20 +70,40 @@ export const POST = withMcpAuth<{ id: string }>('read')(
logger.warn(`[${requestId}] Failed to connect to server ${serverId}:`, error)
}
const now = new Date()
const newStatusConfig =
connectionStatus === 'connected'
? { consecutiveFailures: 0, lastSuccessfulDiscovery: now.toISOString() }
: {
consecutiveFailures: currentStatusConfig.consecutiveFailures + 1,
lastSuccessfulDiscovery: currentStatusConfig.lastSuccessfulDiscovery,
}
const [refreshedServer] = await db
.update(mcpServers)
.set({
lastToolsRefresh: new Date(),
lastToolsRefresh: now,
connectionStatus,
lastError,
lastConnected: connectionStatus === 'connected' ? new Date() : server.lastConnected,
lastConnected: connectionStatus === 'connected' ? now : server.lastConnected,
toolCount,
updatedAt: new Date(),
statusConfig: newStatusConfig,
updatedAt: now,
})
.where(eq(mcpServers.id, serverId))
.returning()
logger.info(`[${requestId}] Successfully refreshed MCP server: ${serverId}`)
if (connectionStatus === 'connected') {
logger.info(
`[${requestId}] Successfully refreshed MCP server: ${serverId} (${toolCount} tools)`
)
await mcpService.clearCache(workspaceId)
} else {
logger.warn(
`[${requestId}] Refresh completed for MCP server ${serverId} but connection failed: ${lastError}`
)
}
return createMcpSuccessResponse({
status: connectionStatus,
toolCount,

View File

@@ -48,6 +48,19 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
// Remove workspaceId from body to prevent it from being updated
const { workspaceId: _, ...updateData } = body
// Get the current server to check if URL is changing
const [currentServer] = await db
.select({ url: mcpServers.url })
.from(mcpServers)
.where(
and(
eq(mcpServers.id, serverId),
eq(mcpServers.workspaceId, workspaceId),
isNull(mcpServers.deletedAt)
)
)
.limit(1)
const [updatedServer] = await db
.update(mcpServers)
.set({
@@ -71,8 +84,12 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
)
}
// Clear MCP service cache after update
mcpService.clearCache(workspaceId)
// Only clear cache if URL changed (requires re-discovery)
const urlChanged = body.url && currentServer?.url !== body.url
if (urlChanged) {
await mcpService.clearCache(workspaceId)
logger.info(`[${requestId}] Cleared cache due to URL change`)
}
logger.info(`[${requestId}] Successfully updated MCP server: ${serverId}`)
return createMcpSuccessResponse({ server: updatedServer })

View File

@@ -117,12 +117,14 @@ export const POST = withMcpAuth('write')(
timeout: body.timeout || 30000,
retries: body.retries || 3,
enabled: body.enabled !== false,
connectionStatus: 'connected',
lastConnected: new Date(),
updatedAt: new Date(),
deletedAt: null,
})
.where(eq(mcpServers.id, serverId))
mcpService.clearCache(workspaceId)
await mcpService.clearCache(workspaceId)
logger.info(
`[${requestId}] Successfully updated MCP server: ${body.name} (ID: ${serverId})`
@@ -145,12 +147,14 @@ export const POST = withMcpAuth('write')(
timeout: body.timeout || 30000,
retries: body.retries || 3,
enabled: body.enabled !== false,
connectionStatus: 'connected',
lastConnected: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
})
.returning()
mcpService.clearCache(workspaceId)
await mcpService.clearCache(workspaceId)
logger.info(
`[${requestId}] Successfully registered MCP server: ${body.name} (ID: ${serverId})`
@@ -212,7 +216,7 @@ export const DELETE = withMcpAuth('admin')(
)
}
mcpService.clearCache(workspaceId)
await mcpService.clearCache(workspaceId)
logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`)
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })

View File

@@ -0,0 +1,103 @@
import { db } from '@sim/db'
import { workflow, workflowBlocks } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { withMcpAuth } from '@/lib/mcp/middleware'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
const logger = createLogger('McpStoredToolsAPI')
export const dynamic = 'force-dynamic'
interface StoredMcpTool {
workflowId: string
workflowName: string
serverId: string
serverUrl?: string
toolName: string
schema?: Record<string, unknown>
}
/**
* GET - Get all stored MCP tools from workflows in the workspace
*
* Scans all workflows in the workspace and extracts MCP tools that have been
* added to agent blocks. Returns the stored state of each tool for comparison
* against current server state.
*/
export const GET = withMcpAuth('read')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
logger.info(`[${requestId}] Fetching stored MCP tools for workspace ${workspaceId}`)
// Get all workflows in workspace
const workflows = await db
.select({
id: workflow.id,
name: workflow.name,
})
.from(workflow)
.where(eq(workflow.workspaceId, workspaceId))
const workflowMap = new Map(workflows.map((w) => [w.id, w.name]))
const workflowIds = workflows.map((w) => w.id)
if (workflowIds.length === 0) {
return createMcpSuccessResponse({ tools: [] })
}
// Get all agent blocks from these workflows
const agentBlocks = await db
.select({
workflowId: workflowBlocks.workflowId,
subBlocks: workflowBlocks.subBlocks,
})
.from(workflowBlocks)
.where(eq(workflowBlocks.type, 'agent'))
const storedTools: StoredMcpTool[] = []
for (const block of agentBlocks) {
if (!workflowMap.has(block.workflowId)) continue
const subBlocks = block.subBlocks as Record<string, unknown> | null
if (!subBlocks) continue
const toolsSubBlock = subBlocks.tools as Record<string, unknown> | undefined
const toolsValue = toolsSubBlock?.value
if (!toolsValue || !Array.isArray(toolsValue)) continue
for (const tool of toolsValue) {
if (tool.type !== 'mcp') continue
const params = tool.params as Record<string, unknown> | undefined
if (!params?.serverId || !params?.toolName) continue
storedTools.push({
workflowId: block.workflowId,
workflowName: workflowMap.get(block.workflowId) || 'Untitled',
serverId: params.serverId as string,
serverUrl: params.serverUrl as string | undefined,
toolName: params.toolName as string,
schema: tool.schema as Record<string, unknown> | undefined,
})
}
}
logger.info(
`[${requestId}] Found ${storedTools.length} stored MCP tools across ${workflows.length} workflows`
)
return createMcpSuccessResponse({ tools: storedTools })
} catch (error) {
logger.error(`[${requestId}] Error fetching stored MCP tools:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to fetch stored MCP tools'),
'Failed to fetch stored MCP tools',
500
)
}
}
)

View File

@@ -3,8 +3,10 @@ import { memory, workflowBlocks } from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { getWorkflowAccessContext } from '@/lib/workflows/utils'
const logger = createLogger('MemoryByIdAPI')
@@ -65,6 +67,65 @@ const memoryPutBodySchema = z.object({
workflowId: z.string().uuid('Invalid workflow ID format'),
})
/**
* Validates authentication and workflow access for memory operations
* @param request - The incoming request
* @param workflowId - The workflow ID to check access for
* @param requestId - Request ID for logging
* @param action - 'read' for GET, 'write' for PUT/DELETE
* @returns Object with userId if successful, or error response if failed
*/
async function validateMemoryAccess(
request: NextRequest,
workflowId: string,
requestId: string,
action: 'read' | 'write'
): Promise<{ userId: string } | { error: NextResponse }> {
const authResult = await checkHybridAuth(request, {
requireWorkflowId: false,
})
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized memory ${action} attempt`)
return {
error: NextResponse.json(
{ success: false, error: { message: 'Authentication required' } },
{ status: 401 }
),
}
}
const accessContext = await getWorkflowAccessContext(workflowId, authResult.userId)
if (!accessContext) {
logger.warn(`[${requestId}] Workflow ${workflowId} not found`)
return {
error: NextResponse.json(
{ success: false, error: { message: 'Workflow not found' } },
{ status: 404 }
),
}
}
const { isOwner, workspacePermission } = accessContext
const hasAccess =
action === 'read'
? isOwner || workspacePermission !== null
: isOwner || workspacePermission === 'write' || workspacePermission === 'admin'
if (!hasAccess) {
logger.warn(
`[${requestId}] User ${authResult.userId} denied ${action} access to workflow ${workflowId}`
)
return {
error: NextResponse.json(
{ success: false, error: { message: 'Access denied' } },
{ status: 403 }
),
}
}
return { userId: authResult.userId }
}
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
@@ -101,6 +162,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const { workflowId: validatedWorkflowId } = validation.data
const accessCheck = await validateMemoryAccess(request, validatedWorkflowId, requestId, 'read')
if ('error' in accessCheck) {
return accessCheck.error
}
const memories = await db
.select()
.from(memory)
@@ -203,6 +269,11 @@ export async function DELETE(
const { workflowId: validatedWorkflowId } = validation.data
const accessCheck = await validateMemoryAccess(request, validatedWorkflowId, requestId, 'write')
if ('error' in accessCheck) {
return accessCheck.error
}
const existingMemory = await db
.select({ id: memory.id })
.from(memory)
@@ -296,6 +367,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
)
}
const accessCheck = await validateMemoryAccess(request, validatedWorkflowId, requestId, 'write')
if ('error' in accessCheck) {
return accessCheck.error
}
const existingMemories = await db
.select()
.from(memory)

View File

@@ -28,7 +28,7 @@ const updateInvitationSchema = z.object({
// Get invitation details
export async function GET(
_req: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ id: string; invitationId: string }> }
) {
const { id: organizationId, invitationId } = await params

View File

@@ -35,6 +35,8 @@ export async function POST(request: NextRequest) {
apiKey,
azureEndpoint,
azureApiVersion,
vertexProject,
vertexLocation,
responseFormat,
workflowId,
workspaceId,
@@ -58,6 +60,8 @@ export async function POST(request: NextRequest) {
hasApiKey: !!apiKey,
hasAzureEndpoint: !!azureEndpoint,
hasAzureApiVersion: !!azureApiVersion,
hasVertexProject: !!vertexProject,
hasVertexLocation: !!vertexLocation,
hasResponseFormat: !!responseFormat,
workflowId,
stream: !!stream,
@@ -104,6 +108,8 @@ export async function POST(request: NextRequest) {
apiKey: finalApiKey,
azureEndpoint,
azureApiVersion,
vertexProject,
vertexLocation,
responseFormat,
workflowId,
workspaceId,

View File

@@ -1,16 +1,19 @@
import { db } from '@sim/db'
import { templates, user } from '@sim/db/schema'
import { templates } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { verifySuperUser } from '@/lib/templates/permissions'
const logger = createLogger('TemplateApprovalAPI')
export const revalidate = 0
// POST /api/templates/[id]/approve - Approve a template (super users only)
/**
* POST /api/templates/[id]/approve - Approve a template (super users only)
*/
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params
@@ -22,23 +25,18 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Check if user is a super user
const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
if (!currentUser[0]?.isSuperUser) {
const { isSuperUser } = await verifySuperUser(session.user.id)
if (!isSuperUser) {
logger.warn(`[${requestId}] Non-super user attempted to approve template: ${id}`)
return NextResponse.json({ error: 'Only super users can approve templates' }, { status: 403 })
}
// Check if template exists
const existingTemplate = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
if (existingTemplate.length === 0) {
logger.warn(`[${requestId}] Template not found for approval: ${id}`)
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
}
// Update template status to approved
await db
.update(templates)
.set({ status: 'approved', updatedAt: new Date() })
@@ -56,9 +54,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
}
}
// POST /api/templates/[id]/reject - Reject a template (super users only)
/**
* DELETE /api/templates/[id]/approve - Unapprove a template (super users only)
*/
export async function DELETE(
request: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = generateRequestId()
@@ -71,23 +71,18 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Check if user is a super user
const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
if (!currentUser[0]?.isSuperUser) {
const { isSuperUser } = await verifySuperUser(session.user.id)
if (!isSuperUser) {
logger.warn(`[${requestId}] Non-super user attempted to reject template: ${id}`)
return NextResponse.json({ error: 'Only super users can reject templates' }, { status: 403 })
}
// Check if template exists
const existingTemplate = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
if (existingTemplate.length === 0) {
logger.warn(`[${requestId}] Template not found for rejection: ${id}`)
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
}
// Update template status to rejected
await db
.update(templates)
.set({ status: 'rejected', updatedAt: new Date() })

View File

@@ -0,0 +1,142 @@
import { db } from '@sim/db'
import { templates } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
import { verifyTemplateOwnership } from '@/lib/templates/permissions'
import { uploadFile } from '@/lib/uploads/core/storage-service'
import { isValidPng } from '@/lib/uploads/utils/validation'
const logger = createLogger('TemplateOGImageAPI')
/**
* PUT /api/templates/[id]/og-image
* Upload a pre-generated OG image for a template.
* Accepts base64-encoded image data in the request body.
*/
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized OG image upload attempt for template: ${id}`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { authorized, error, status } = await verifyTemplateOwnership(
id,
session.user.id,
'admin'
)
if (!authorized) {
logger.warn(`[${requestId}] User denied permission to upload OG image for template ${id}`)
return NextResponse.json({ error }, { status: status || 403 })
}
const body = await request.json()
const { imageData } = body
if (!imageData || typeof imageData !== 'string') {
return NextResponse.json(
{ error: 'Missing or invalid imageData (expected base64 string)' },
{ status: 400 }
)
}
const base64Data = imageData.includes(',') ? imageData.split(',')[1] : imageData
const imageBuffer = Buffer.from(base64Data, 'base64')
if (!isValidPng(imageBuffer)) {
return NextResponse.json({ error: 'Invalid PNG image data' }, { status: 400 })
}
const maxSize = 5 * 1024 * 1024
if (imageBuffer.length > maxSize) {
return NextResponse.json({ error: 'Image too large. Maximum size is 5MB.' }, { status: 400 })
}
const timestamp = Date.now()
const storageKey = `og-images/templates/${id}/${timestamp}.png`
logger.info(`[${requestId}] Uploading OG image for template ${id}: ${storageKey}`)
const uploadResult = await uploadFile({
file: imageBuffer,
fileName: storageKey,
contentType: 'image/png',
context: 'og-images',
preserveKey: true,
customKey: storageKey,
})
const baseUrl = getBaseUrl()
const ogImageUrl = `${baseUrl}${uploadResult.path}?context=og-images`
await db
.update(templates)
.set({
ogImageUrl,
updatedAt: new Date(),
})
.where(eq(templates.id, id))
logger.info(`[${requestId}] Successfully uploaded OG image for template ${id}: ${ogImageUrl}`)
return NextResponse.json({
success: true,
ogImageUrl,
})
} catch (error: unknown) {
logger.error(`[${requestId}] Error uploading OG image for template ${id}:`, error)
return NextResponse.json({ error: 'Failed to upload OG image' }, { status: 500 })
}
}
/**
* DELETE /api/templates/[id]/og-image
* Remove the OG image for a template.
*/
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = generateRequestId()
const { id } = await params
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { authorized, error, status } = await verifyTemplateOwnership(
id,
session.user.id,
'admin'
)
if (!authorized) {
logger.warn(`[${requestId}] User denied permission to delete OG image for template ${id}`)
return NextResponse.json({ error }, { status: status || 403 })
}
await db
.update(templates)
.set({
ogImageUrl: null,
updatedAt: new Date(),
})
.where(eq(templates.id, id))
logger.info(`[${requestId}] Removed OG image for template ${id}`)
return NextResponse.json({ success: true })
} catch (error: unknown) {
logger.error(`[${requestId}] Error removing OG image for template ${id}:`, error)
return NextResponse.json({ error: 'Failed to remove OG image' }, { status: 500 })
}
}

View File

@@ -1,16 +1,19 @@
import { db } from '@sim/db'
import { templates, user } from '@sim/db/schema'
import { templates } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { verifySuperUser } from '@/lib/templates/permissions'
const logger = createLogger('TemplateRejectionAPI')
export const revalidate = 0
// POST /api/templates/[id]/reject - Reject a template (super users only)
/**
* POST /api/templates/[id]/reject - Reject a template (super users only)
*/
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params
@@ -22,23 +25,18 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Check if user is a super user
const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
if (!currentUser[0]?.isSuperUser) {
const { isSuperUser } = await verifySuperUser(session.user.id)
if (!isSuperUser) {
logger.warn(`[${requestId}] Non-super user attempted to reject template: ${id}`)
return NextResponse.json({ error: 'Only super users can reject templates' }, { status: 403 })
}
// Check if template exists
const existingTemplate = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
if (existingTemplate.length === 0) {
logger.warn(`[${requestId}] Template not found for rejection: ${id}`)
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
}
// Update template status to rejected
await db
.update(templates)
.set({ status: 'rejected', updatedAt: new Date() })

View File

@@ -1,6 +1,6 @@
import { db } from '@sim/db'
import { member, templateCreators, templates, workflow } from '@sim/db/schema'
import { and, eq, or, sql } from 'drizzle-orm'
import { templateCreators, templates, workflow } from '@sim/db/schema'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
@@ -15,7 +15,6 @@ const logger = createLogger('TemplateByIdAPI')
export const revalidate = 0
// GET /api/templates/[id] - Retrieve a single template by ID
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params
@@ -25,7 +24,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
logger.debug(`[${requestId}] Fetching template: ${id}`)
// Fetch the template by ID with creator info
const result = await db
.select({
template: templates,
@@ -47,12 +45,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
creator: creator || undefined,
}
// Only show approved templates to non-authenticated users
if (!session?.user?.id && template.status !== 'approved') {
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
}
// Check if user has starred (only if authenticated)
let isStarred = false
if (session?.user?.id) {
const { templateStars } = await import('@sim/db/schema')
@@ -80,7 +76,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
logger.debug(`[${requestId}] Incremented view count for template: ${id}`)
} catch (viewError) {
// Log the error but don't fail the request
logger.warn(`[${requestId}] Failed to increment view count for template: ${id}`, viewError)
}
}
@@ -138,7 +133,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
const { name, details, creatorId, tags, updateState } = validationResult.data
// Check if template exists
const existingTemplate = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
if (existingTemplate.length === 0) {
@@ -146,32 +140,54 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
}
// No permission check needed - template updates only happen from within the workspace
// where the user is already editing the connected workflow
const template = existingTemplate[0]
if (!template.creatorId) {
logger.warn(`[${requestId}] Template ${id} has no creator, denying update`)
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
const { verifyCreatorPermission } = await import('@/lib/templates/permissions')
const { hasPermission, error: permissionError } = await verifyCreatorPermission(
session.user.id,
template.creatorId,
'admin'
)
if (!hasPermission) {
logger.warn(`[${requestId}] User denied permission to update template ${id}`)
return NextResponse.json({ error: permissionError || 'Access denied' }, { status: 403 })
}
// Prepare update data - only include fields that were provided
const updateData: any = {
updatedAt: new Date(),
}
// Only update fields that were provided
if (name !== undefined) updateData.name = name
if (details !== undefined) updateData.details = details
if (tags !== undefined) updateData.tags = tags
if (creatorId !== undefined) updateData.creatorId = creatorId
// Only update the state if explicitly requested and the template has a connected workflow
if (updateState && existingTemplate[0].workflowId) {
// Load the current workflow state from normalized tables
if (updateState && template.workflowId) {
const { verifyWorkflowAccess } = await import('@/socket-server/middleware/permissions')
const { hasAccess: hasWorkflowAccess } = await verifyWorkflowAccess(
session.user.id,
template.workflowId
)
if (!hasWorkflowAccess) {
logger.warn(`[${requestId}] User denied workflow access for state sync on template ${id}`)
return NextResponse.json({ error: 'Access denied to workflow' }, { status: 403 })
}
const { loadWorkflowFromNormalizedTables } = await import('@/lib/workflows/persistence/utils')
const normalizedData = await loadWorkflowFromNormalizedTables(existingTemplate[0].workflowId)
const normalizedData = await loadWorkflowFromNormalizedTables(template.workflowId)
if (normalizedData) {
// Also fetch workflow variables
const [workflowRecord] = await db
.select({ variables: workflow.variables })
.from(workflow)
.where(eq(workflow.id, existingTemplate[0].workflowId))
.where(eq(workflow.id, template.workflowId))
.limit(1)
const currentState = {
@@ -183,17 +199,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
lastSaved: Date.now(),
}
// Extract credential requirements from the new state
const requiredCredentials = extractRequiredCredentials(currentState)
// Sanitize the state before storing
const sanitizedState = sanitizeCredentials(currentState)
updateData.state = sanitizedState
updateData.requiredCredentials = requiredCredentials
logger.info(
`[${requestId}] Updating template state and credentials from current workflow: ${existingTemplate[0].workflowId}`
`[${requestId}] Updating template state and credentials from current workflow: ${template.workflowId}`
)
} else {
logger.warn(`[${requestId}] Could not load workflow state for template: ${id}`)
@@ -233,7 +247,6 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Fetch template
const existing = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
if (existing.length === 0) {
logger.warn(`[${requestId}] Template not found for delete: ${id}`)
@@ -242,41 +255,21 @@ export async function DELETE(
const template = existing[0]
// Permission: Only admin/owner of creator profile can delete
if (template.creatorId) {
const creatorProfile = await db
.select()
.from(templateCreators)
.where(eq(templateCreators.id, template.creatorId))
.limit(1)
if (!template.creatorId) {
logger.warn(`[${requestId}] Template ${id} has no creator, denying delete`)
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
if (creatorProfile.length > 0) {
const creator = creatorProfile[0]
let hasPermission = false
const { verifyCreatorPermission } = await import('@/lib/templates/permissions')
const { hasPermission, error: permissionError } = await verifyCreatorPermission(
session.user.id,
template.creatorId,
'admin'
)
if (creator.referenceType === 'user') {
hasPermission = creator.referenceId === session.user.id
} else if (creator.referenceType === 'organization') {
// For delete, require admin/owner role
const membership = await db
.select()
.from(member)
.where(
and(
eq(member.userId, session.user.id),
eq(member.organizationId, creator.referenceId),
or(eq(member.role, 'admin'), eq(member.role, 'owner'))
)
)
.limit(1)
hasPermission = membership.length > 0
}
if (!hasPermission) {
logger.warn(`[${requestId}] User denied permission to delete template ${id}`)
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
}
if (!hasPermission) {
logger.warn(`[${requestId}] User denied permission to delete template ${id}`)
return NextResponse.json({ error: permissionError || 'Access denied' }, { status: 403 })
}
await db.delete(templates).where(eq(templates.id, id))

View File

@@ -1,6 +1,5 @@
import { db } from '@sim/db'
import {
member,
templateCreators,
templateStars,
templates,
@@ -204,51 +203,18 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
}
// Validate creator profile - required for all templates
const creatorProfile = await db
.select()
.from(templateCreators)
.where(eq(templateCreators.id, data.creatorId))
.limit(1)
const { verifyCreatorPermission } = await import('@/lib/templates/permissions')
const { hasPermission, error: permissionError } = await verifyCreatorPermission(
session.user.id,
data.creatorId,
'member'
)
if (creatorProfile.length === 0) {
logger.warn(`[${requestId}] Creator profile not found: ${data.creatorId}`)
return NextResponse.json({ error: 'Creator profile not found' }, { status: 404 })
if (!hasPermission) {
logger.warn(`[${requestId}] User cannot use creator profile: ${data.creatorId}`)
return NextResponse.json({ error: permissionError || 'Access denied' }, { status: 403 })
}
const creator = creatorProfile[0]
// Verify user has permission to use this creator profile
if (creator.referenceType === 'user') {
if (creator.referenceId !== session.user.id) {
logger.warn(`[${requestId}] User cannot use creator profile: ${data.creatorId}`)
return NextResponse.json(
{ error: 'You do not have permission to use this creator profile' },
{ status: 403 }
)
}
} else if (creator.referenceType === 'organization') {
// Verify user is a member of the organization
const membership = await db
.select()
.from(member)
.where(
and(eq(member.userId, session.user.id), eq(member.organizationId, creator.referenceId))
)
.limit(1)
if (membership.length === 0) {
logger.warn(
`[${requestId}] User not a member of organization for creator: ${data.creatorId}`
)
return NextResponse.json(
{ error: 'You must be a member of the organization to use its creator profile' },
{ status: 403 }
)
}
}
// Create the template
const templateId = uuidv4()
const now = new Date()

View File

@@ -1,6 +1,7 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -108,6 +109,14 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
if (folderId) {
const folderIdValidation = validateAlphanumericId(folderId, 'folderId', 50)
if (!folderIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid folderId`, { error: folderIdValidation.error })
return NextResponse.json({ error: folderIdValidation.error }, { status: 400 })
}
}
const qParts: string[] = ['trashed = false']
if (folderId) {
qParts.push(`'${escapeForDriveQuery(folderId)}' in parents`)

View File

@@ -1,6 +1,7 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
@@ -50,6 +51,29 @@ export async function POST(request: NextRequest) {
.map((id) => id.trim())
.filter((id) => id.length > 0)
for (const labelId of labelIds) {
const labelIdValidation = validateAlphanumericId(labelId, 'labelId', 255)
if (!labelIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid label ID: ${labelIdValidation.error}`)
return NextResponse.json(
{
success: false,
error: labelIdValidation.error,
},
{ status: 400 }
)
}
}
const messageIdValidation = validateAlphanumericId(validatedData.messageId, 'messageId', 255)
if (!messageIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid message ID: ${messageIdValidation.error}`)
return NextResponse.json(
{ success: false, error: messageIdValidation.error },
{ status: 400 }
)
}
const gmailResponse = await fetch(
`${GMAIL_API_BASE}/messages/${validatedData.messageId}/modify`,
{

View File

@@ -3,6 +3,7 @@ import { account } from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -38,6 +39,12 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
if (!credentialIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
}
let credentials = await db
.select()
.from(account)

View File

@@ -1,6 +1,7 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
@@ -53,6 +54,29 @@ export async function POST(request: NextRequest) {
.map((id) => id.trim())
.filter((id) => id.length > 0)
for (const labelId of labelIds) {
const labelIdValidation = validateAlphanumericId(labelId, 'labelId', 255)
if (!labelIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid label ID: ${labelIdValidation.error}`)
return NextResponse.json(
{
success: false,
error: labelIdValidation.error,
},
{ status: 400 }
)
}
}
const messageIdValidation = validateAlphanumericId(validatedData.messageId, 'messageId', 255)
if (!messageIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid message ID: ${messageIdValidation.error}`)
return NextResponse.json(
{ success: false, error: messageIdValidation.error },
{ status: 400 }
)
}
const gmailResponse = await fetch(
`${GMAIL_API_BASE}/messages/${validatedData.messageId}/modify`,
{

View File

@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateUUID } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -25,7 +26,6 @@ export async function GET(request: NextRequest) {
logger.info(`[${requestId}] Google Calendar calendars request received`)
try {
// Get the credential ID from the query params
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const workflowId = searchParams.get('workflowId') || undefined
@@ -34,12 +34,25 @@ export async function GET(request: NextRequest) {
logger.warn(`[${requestId}] Missing credentialId parameter`)
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
const credentialValidation = validateUUID(credentialId, 'credentialId')
if (!credentialValidation.isValid) {
logger.warn(`[${requestId}] Invalid credentialId format`, { credentialId })
return NextResponse.json({ error: credentialValidation.error }, { status: 400 })
}
if (workflowId) {
const workflowValidation = validateUUID(workflowId, 'workflowId')
if (!workflowValidation.isValid) {
logger.warn(`[${requestId}] Invalid workflowId format`, { workflowId })
return NextResponse.json({ error: workflowValidation.error }, { status: 400 })
}
}
const authz = await authorizeCredentialUse(request, { credentialId, workflowId })
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
// Refresh access token if needed using the utility function
const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
authz.credentialOwnerUserId,
@@ -50,7 +63,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Fetch calendars from Google Calendar API
logger.info(`[${requestId}] Fetching calendars from Google Calendar API`)
const calendarResponse = await fetch(
'https://www.googleapis.com/calendar/v3/users/me/calendarList',
@@ -81,7 +93,6 @@ export async function GET(request: NextRequest) {
const data = await calendarResponse.json()
const calendars: CalendarListItem[] = data.items || []
// Sort calendars with primary first, then alphabetically
calendars.sort((a, b) => {
if (a.primary && !b.primary) return -1
if (!a.primary && b.primary) return 1

View File

@@ -20,6 +20,12 @@ export async function POST(request: Request) {
cloudId: providedCloudId,
issueType,
parent,
labels,
duedate,
reporter,
environment,
customFieldId,
customFieldValue,
} = await request.json()
if (!domain) {
@@ -94,17 +100,57 @@ export async function POST(request: Request) {
}
if (priority !== undefined && priority !== null && priority !== '') {
fields.priority = {
name: priority,
const isNumericId = /^\d+$/.test(priority)
fields.priority = isNumericId ? { id: priority } : { name: priority }
}
if (labels !== undefined && labels !== null && Array.isArray(labels) && labels.length > 0) {
fields.labels = labels
}
if (duedate !== undefined && duedate !== null && duedate !== '') {
fields.duedate = duedate
}
if (reporter !== undefined && reporter !== null && reporter !== '') {
fields.reporter = {
id: reporter,
}
}
if (assignee !== undefined && assignee !== null && assignee !== '') {
fields.assignee = {
id: assignee,
if (environment !== undefined && environment !== null && environment !== '') {
fields.environment = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: environment,
},
],
},
],
}
}
if (
customFieldId !== undefined &&
customFieldId !== null &&
customFieldId !== '' &&
customFieldValue !== undefined &&
customFieldValue !== null &&
customFieldValue !== ''
) {
const fieldId = customFieldId.startsWith('customfield_')
? customFieldId
: `customfield_${customFieldId}`
fields[fieldId] = customFieldValue
}
const body = { fields }
const response = await fetch(url, {
@@ -132,16 +178,47 @@ export async function POST(request: Request) {
}
const responseData = await response.json()
logger.info('Successfully created Jira issue:', responseData.key)
const issueKey = responseData.key || 'unknown'
logger.info('Successfully created Jira issue:', issueKey)
let assigneeId: string | undefined
if (assignee !== undefined && assignee !== null && assignee !== '') {
const assignUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueKey}/assignee`
logger.info('Assigning issue to:', assignee)
const assignResponse = await fetch(assignUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
accountId: assignee,
}),
})
if (!assignResponse.ok) {
const assignErrorText = await assignResponse.text()
logger.warn('Failed to assign issue (issue was created successfully):', {
status: assignResponse.status,
error: assignErrorText,
})
} else {
assigneeId = assignee
logger.info('Successfully assigned issue to:', assignee)
}
}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueKey: responseData.key || 'unknown',
issueKey: issueKey,
summary: responseData.fields?.summary || 'Issue created',
success: true,
url: `https://${domain}/browse/${responseData.key}`,
url: `https://${domain}/browse/${issueKey}`,
...(assigneeId && { assigneeId }),
},
})
} catch (error: any) {

View File

@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -23,6 +24,12 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Team ID is required' }, { status: 400 })
}
const teamIdValidation = validateMicrosoftGraphId(teamId, 'Team ID')
if (!teamIdValidation.isValid) {
logger.warn('Invalid team ID provided', { teamId, error: teamIdValidation.error })
return NextResponse.json({ error: teamIdValidation.error }, { status: 400 })
}
try {
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
@@ -70,7 +77,6 @@ export async function POST(request: Request) {
endpoint: `https://graph.microsoft.com/v1.0/teams/${teamId}/channels`,
})
// Check for auth errors specifically
if (response.status === 401) {
return NextResponse.json(
{
@@ -93,7 +99,6 @@ export async function POST(request: Request) {
} catch (innerError) {
logger.error('Error during API requests:', innerError)
// Check if it's an authentication error
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
if (
errorMessage.includes('auth') ||

View File

@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -7,21 +8,35 @@ export const dynamic = 'force-dynamic'
const logger = createLogger('TeamsChatsAPI')
// Helper function to get chat members and create a meaningful name
/**
* Helper function to get chat members and create a meaningful name
*
* @param chatId - Microsoft Teams chat ID to get display name for
* @param accessToken - Access token for Microsoft Graph API
* @param chatTopic - Optional existing chat topic
* @returns A meaningful display name for the chat
*/
const getChatDisplayName = async (
chatId: string,
accessToken: string,
chatTopic?: string
): Promise<string> => {
try {
// If the chat already has a topic, use it
const chatIdValidation = validateMicrosoftGraphId(chatId, 'chatId')
if (!chatIdValidation.isValid) {
logger.warn('Invalid chat ID in getChatDisplayName', {
error: chatIdValidation.error,
chatId: chatId.substring(0, 50),
})
return `Chat ${chatId.substring(0, 8)}...`
}
if (chatTopic?.trim() && chatTopic !== 'null') {
return chatTopic
}
// Fetch chat members to create a meaningful name
const membersResponse = await fetch(
`https://graph.microsoft.com/v1.0/chats/${chatId}/members`,
`https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/members`,
{
method: 'GET',
headers: {
@@ -35,27 +50,25 @@ const getChatDisplayName = async (
const membersData = await membersResponse.json()
const members = membersData.value || []
// Filter out the current user and get display names
const memberNames = members
.filter((member: any) => member.displayName && member.displayName !== 'Unknown')
.map((member: any) => member.displayName)
.slice(0, 3) // Limit to first 3 names to avoid very long names
.slice(0, 3)
if (memberNames.length > 0) {
if (memberNames.length === 1) {
return memberNames[0] // 1:1 chat
return memberNames[0]
}
if (memberNames.length === 2) {
return memberNames.join(' & ') // 2-person group
return memberNames.join(' & ')
}
return `${memberNames.slice(0, 2).join(', ')} & ${memberNames.length - 2} more` // Larger group
return `${memberNames.slice(0, 2).join(', ')} & ${memberNames.length - 2} more`
}
}
// Fallback: try to get a better name from recent messages
try {
const messagesResponse = await fetch(
`https://graph.microsoft.com/v1.0/chats/${chatId}/messages?$top=10&$orderby=createdDateTime desc`,
`https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages?$top=10&$orderby=createdDateTime desc`,
{
method: 'GET',
headers: {
@@ -69,14 +82,12 @@ const getChatDisplayName = async (
const messagesData = await messagesResponse.json()
const messages = messagesData.value || []
// Look for chat rename events
for (const message of messages) {
if (message.eventDetail?.chatDisplayName) {
return message.eventDetail.chatDisplayName
}
}
// Get unique sender names from recent messages as last resort
const senderNames = [
...new Set(
messages
@@ -103,7 +114,6 @@ const getChatDisplayName = async (
)
}
// Final fallback
return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...`
} catch (error) {
logger.warn(
@@ -146,7 +156,6 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Could not retrieve access token' }, { status: 401 })
}
// Now try to fetch the chats
const response = await fetch('https://graph.microsoft.com/v1.0/me/chats', {
method: 'GET',
headers: {
@@ -163,7 +172,6 @@ export async function POST(request: Request) {
endpoint: 'https://graph.microsoft.com/v1.0/me/chats',
})
// Check for auth errors specifically
if (response.status === 401) {
return NextResponse.json(
{
@@ -179,7 +187,6 @@ export async function POST(request: Request) {
const data = await response.json()
// Process chats with enhanced display names
const chats = await Promise.all(
data.value.map(async (chat: any) => ({
id: chat.id,
@@ -193,7 +200,6 @@ export async function POST(request: Request) {
} catch (innerError) {
logger.error('Error during API requests:', innerError)
// Check if it's an authentication error
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
if (
errorMessage.includes('auth') ||

View File

@@ -30,23 +30,41 @@ export async function createMongoDBConnection(config: MongoDBConnectionConfig) {
return client
}
/**
* Recursively checks an object for dangerous MongoDB operators
* @param obj - The object to check
* @param dangerousOperators - Array of operator names to block
* @returns true if a dangerous operator is found
*/
function containsDangerousOperator(obj: unknown, dangerousOperators: string[]): boolean {
if (typeof obj !== 'object' || obj === null) return false
for (const key of Object.keys(obj as Record<string, unknown>)) {
if (dangerousOperators.includes(key)) return true
if (
typeof (obj as Record<string, unknown>)[key] === 'object' &&
containsDangerousOperator((obj as Record<string, unknown>)[key], dangerousOperators)
) {
return true
}
}
return false
}
export function validateFilter(filter: string): { isValid: boolean; error?: string } {
try {
const parsed = JSON.parse(filter)
const dangerousOperators = ['$where', '$regex', '$expr', '$function', '$accumulator', '$let']
const dangerousOperators = [
'$where', // Executes arbitrary JavaScript
'$regex', // Can cause ReDoS attacks
'$expr', // Expression evaluation
'$function', // Custom JavaScript functions
'$accumulator', // Custom JavaScript accumulators
'$let', // Variable definitions that could be exploited
]
const checkForDangerousOps = (obj: any): boolean => {
if (typeof obj !== 'object' || obj === null) return false
for (const key of Object.keys(obj)) {
if (dangerousOperators.includes(key)) return true
if (typeof obj[key] === 'object' && checkForDangerousOps(obj[key])) return true
}
return false
}
if (checkForDangerousOps(parsed)) {
if (containsDangerousOperator(parsed, dangerousOperators)) {
return {
isValid: false,
error: 'Filter contains potentially dangerous operators',
@@ -74,29 +92,19 @@ export function validatePipeline(pipeline: string): { isValid: boolean; error?:
}
const dangerousOperators = [
'$where',
'$function',
'$accumulator',
'$let',
'$merge',
'$out',
'$currentOp',
'$listSessions',
'$listLocalSessions',
'$where', // Executes arbitrary JavaScript
'$function', // Custom JavaScript functions
'$accumulator', // Custom JavaScript accumulators
'$let', // Variable definitions that could be exploited
'$merge', // Writes to external collections
'$out', // Writes to external collections
'$currentOp', // Exposes system operation info
'$listSessions', // Exposes session info
'$listLocalSessions', // Exposes local session info
]
const checkPipelineStage = (stage: any): boolean => {
if (typeof stage !== 'object' || stage === null) return false
for (const key of Object.keys(stage)) {
if (dangerousOperators.includes(key)) return true
if (typeof stage[key] === 'object' && checkPipelineStage(stage[key])) return true
}
return false
}
for (const stage of parsed) {
if (checkPipelineStage(stage)) {
if (containsDangerousOperator(stage, dangerousOperators)) {
return {
isValid: false,
error: 'Pipeline contains potentially dangerous operators',

View File

@@ -98,15 +98,45 @@ export function buildDeleteQuery(table: string, where: string) {
return { query, values: [] }
}
/**
* Validates a WHERE clause to prevent SQL injection attacks
* @param where - The WHERE clause string to validate
* @throws {Error} If the WHERE clause contains potentially dangerous patterns
*/
function validateWhereClause(where: string): void {
const dangerousPatterns = [
// DDL and DML injection via stacked queries
/;\s*(drop|delete|insert|update|create|alter|grant|revoke)/i,
/union\s+select/i,
// Union-based injection
/union\s+(all\s+)?select/i,
// File operations
/into\s+outfile/i,
/load_file/i,
/into\s+dumpfile/i,
/load_file\s*\(/i,
// Comment-based injection (can truncate query)
/--/,
/\/\*/,
/\*\//,
// Tautologies - always true/false conditions using backreferences
// Matches OR 'x'='x' or OR x=x (same value both sides) but NOT OR col='value'
/\bor\s+(['"]?)(\w+)\1\s*=\s*\1\2\1/i,
/\bor\s+true\b/i,
/\bor\s+false\b/i,
// AND tautologies (less common but still used in attacks)
/\band\s+(['"]?)(\w+)\1\s*=\s*\1\2\1/i,
/\band\s+true\b/i,
/\band\s+false\b/i,
// Time-based blind injection
/\bsleep\s*\(/i,
/\bbenchmark\s*\(/i,
/\bwaitfor\s+delay/i,
// Stacked queries (any statement after semicolon)
/;\s*\w+/,
// Information schema queries
/information_schema/i,
/mysql\./i,
// System functions and procedures
/\bxp_cmdshell/i,
]
for (const pattern of dangerousPatterns) {

View File

@@ -4,6 +4,7 @@ import { account } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -36,6 +37,12 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
const credentialIdValidation = validateMicrosoftGraphId(credentialId, 'credentialId')
if (!credentialIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error })
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
}
logger.info(`[${requestId}] Fetching credential`, { credentialId })
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)

View File

@@ -4,6 +4,7 @@ import { account } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -33,6 +34,12 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
const credentialIdValidation = validateMicrosoftGraphId(credentialId, 'credentialId')
if (!credentialIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error })
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
}
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
@@ -48,7 +55,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Build URL for OneDrive folders
let url = `https://graph.microsoft.com/v1.0/me/drive/root/children?$filter=folder ne null&$select=id,name,folder,webUrl,createdDateTime,lastModifiedDateTime&$top=50`
if (query) {
@@ -71,7 +77,7 @@ export async function GET(request: NextRequest) {
const data = await response.json()
const folders = (data.value || [])
.filter((item: MicrosoftGraphDriveItem) => item.folder) // Only folders
.filter((item: MicrosoftGraphDriveItem) => item.folder)
.map((folder: MicrosoftGraphDriveItem) => ({
id: folder.id,
name: folder.name,

View File

@@ -2,6 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import * as XLSX from 'xlsx'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import {
@@ -28,9 +29,9 @@ const ExcelValuesSchema = z.union([
const OneDriveUploadSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
fileName: z.string().min(1, 'File name is required'),
file: z.any().optional(), // UserFile object (optional for blank Excel creation)
file: z.any().optional(),
folderId: z.string().optional().nullable(),
mimeType: z.string().nullish(), // Accept string, null, or undefined
mimeType: z.string().nullish(),
values: ExcelValuesSchema.optional().nullable(),
})
@@ -62,24 +63,19 @@ export async function POST(request: NextRequest) {
let fileBuffer: Buffer
let mimeType: string
// Check if we're creating a blank Excel file
const isExcelCreation =
validatedData.mimeType ===
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' && !validatedData.file
if (isExcelCreation) {
// Create a blank Excel workbook
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.aoa_to_sheet([[]])
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
// Generate XLSX file as buffer
const xlsxBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' })
fileBuffer = Buffer.from(xlsxBuffer)
mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
} else {
// Handle regular file upload
const rawFile = validatedData.file
if (!rawFile) {
@@ -108,7 +104,6 @@ export async function POST(request: NextRequest) {
fileToProcess = rawFile
}
// Convert to UserFile format
let userFile
try {
userFile = processSingleFileToUserFile(fileToProcess, requestId, logger)
@@ -138,7 +133,7 @@ export async function POST(request: NextRequest) {
mimeType = userFile.type || 'application/octet-stream'
}
const maxSize = 250 * 1024 * 1024 // 250MB
const maxSize = 250 * 1024 * 1024
if (fileBuffer.length > maxSize) {
const sizeMB = (fileBuffer.length / (1024 * 1024)).toFixed(2)
logger.warn(`[${requestId}] File too large: ${sizeMB}MB`)
@@ -151,7 +146,6 @@ export async function POST(request: NextRequest) {
)
}
// Ensure file name has an appropriate extension
let fileName = validatedData.fileName
const hasExtension = fileName.includes('.') && fileName.lastIndexOf('.') > 0
@@ -169,6 +163,17 @@ export async function POST(request: NextRequest) {
const folderId = validatedData.folderId?.trim()
if (folderId && folderId !== '') {
const folderIdValidation = validateMicrosoftGraphId(folderId, 'folderId')
if (!folderIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid folder ID`, { error: folderIdValidation.error })
return NextResponse.json(
{
success: false,
error: folderIdValidation.error,
},
{ status: 400 }
)
}
uploadUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(folderId)}:/${encodeURIComponent(fileName)}:/content`
} else {
uploadUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/root:/${encodeURIComponent(fileName)}:/content`
@@ -197,14 +202,12 @@ export async function POST(request: NextRequest) {
const fileData = await uploadResponse.json()
// If this is an Excel creation and values were provided, write them using the Excel API
let excelWriteResult: any | undefined
const shouldWriteExcelContent =
isExcelCreation && Array.isArray(excelValues) && excelValues.length > 0
if (shouldWriteExcelContent) {
try {
// Create a workbook session to ensure reliability and persistence of changes
let workbookSessionId: string | undefined
const sessionResp = await fetch(
`${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(fileData.id)}/workbook/createSession`,
@@ -223,7 +226,6 @@ export async function POST(request: NextRequest) {
workbookSessionId = sessionData?.id
}
// Determine the first worksheet name
let sheetName = 'Sheet1'
try {
const listUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(
@@ -272,7 +274,6 @@ export async function POST(request: NextRequest) {
return paddedRow
})
// Compute concise end range from A1 and matrix size (no network round-trip)
const indexToColLetters = (index: number): string => {
let n = index
let s = ''
@@ -313,7 +314,6 @@ export async function POST(request: NextRequest) {
statusText: excelWriteResponse?.statusText,
error: errorText,
})
// Do not fail the entire request; return upload success with write error details
excelWriteResult = {
success: false,
error: `Excel write failed: ${excelWriteResponse?.statusText || 'unknown'}`,
@@ -321,7 +321,6 @@ export async function POST(request: NextRequest) {
}
} else {
const writeData = await excelWriteResponse.json()
// The Range PATCH returns a Range object; log address and values length
const addr = writeData.address || writeData.addressLocal
const v = writeData.values || []
excelWriteResult = {
@@ -333,7 +332,6 @@ export async function POST(request: NextRequest) {
}
}
// Attempt to close the workbook session if one was created
if (workbookSessionId) {
try {
const closeResp = await fetch(

View File

@@ -3,6 +3,7 @@ import { account } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -29,8 +30,13 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId')
if (!credentialIdValidation.isValid) {
logger.warn('Invalid credentialId format', { error: credentialIdValidation.error })
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
}
try {
// Ensure we have a session for permission checks
const sessionUserId = session?.user?.id || ''
if (!sessionUserId) {
@@ -38,7 +44,6 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}
// Resolve the credential owner to support collaborator-owned credentials
const creds = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!creds.length) {
logger.warn('Credential not found', { credentialId })
@@ -79,7 +84,6 @@ export async function GET(request: Request) {
endpoint: 'https://graph.microsoft.com/v1.0/me/mailFolders',
})
// Check for auth errors specifically
if (response.status === 401) {
return NextResponse.json(
{
@@ -96,7 +100,6 @@ export async function GET(request: Request) {
const data = await response.json()
const folders = data.value || []
// Transform folders to match the expected format
const transformedFolders = folders.map((folder: OutlookFolder) => ({
id: folder.id,
name: folder.displayName,
@@ -111,7 +114,6 @@ export async function GET(request: Request) {
} catch (innerError) {
logger.error('Error during API requests:', innerError)
// Check if it's an authentication error
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
if (
errorMessage.includes('auth') ||

View File

@@ -64,15 +64,46 @@ export function sanitizeIdentifier(identifier: string): string {
return sanitizeSingleIdentifier(identifier)
}
/**
* Validates a WHERE clause to prevent SQL injection attacks
* @param where - The WHERE clause string to validate
* @throws {Error} If the WHERE clause contains potentially dangerous patterns
*/
function validateWhereClause(where: string): void {
const dangerousPatterns = [
// DDL and DML injection via stacked queries
/;\s*(drop|delete|insert|update|create|alter|grant|revoke)/i,
/union\s+select/i,
// Union-based injection
/union\s+(all\s+)?select/i,
// File operations
/into\s+outfile/i,
/load_file/i,
/load_file\s*\(/i,
/pg_read_file/i,
// Comment-based injection (can truncate query)
/--/,
/\/\*/,
/\*\//,
// Tautologies - always true/false conditions using backreferences
// Matches OR 'x'='x' or OR x=x (same value both sides) but NOT OR col='value'
/\bor\s+(['"]?)(\w+)\1\s*=\s*\1\2\1/i,
/\bor\s+true\b/i,
/\bor\s+false\b/i,
// AND tautologies (less common but still used in attacks)
/\band\s+(['"]?)(\w+)\1\s*=\s*\1\2\1/i,
/\band\s+true\b/i,
/\band\s+false\b/i,
// Time-based blind injection
/\bsleep\s*\(/i,
/\bwaitfor\s+delay/i,
/\bpg_sleep\s*\(/i,
/\bbenchmark\s*\(/i,
// Stacked queries (any statement after semicolon)
/;\s*\w+/,
// Information schema / system catalog queries
/information_schema/i,
/pg_catalog/i,
// System functions and procedures
/\bxp_cmdshell/i,
]
for (const pattern of dangerousPatterns) {

View File

@@ -4,6 +4,7 @@ import { account } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import type { SharepointSite } from '@/tools/sharepoint/types'
@@ -32,6 +33,12 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
if (!credentialIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error })
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
}
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
@@ -47,8 +54,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Build URL for SharePoint sites
// Use search=* to get all sites the user has access to, or search for specific query
const searchQuery = query || '*'
const url = `https://graph.microsoft.com/v1.0/sites?search=${encodeURIComponent(searchQuery)}&$select=id,name,displayName,webUrl,createdDateTime,lastModifiedDateTime&$top=50`

View File

@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -93,7 +94,6 @@ export async function POST(request: Request) {
}
}
// Filter to channels the bot can access and format the response
const channels = (data.channels || [])
.filter((channel: SlackChannel) => {
const canAccess = !channel.is_archived && (channel.is_member || !channel.is_private)
@@ -106,6 +106,28 @@ export async function POST(request: Request) {
return canAccess
})
.filter((channel: SlackChannel) => {
const validation = validateAlphanumericId(channel.id, 'channelId', 50)
if (!validation.isValid) {
logger.warn('Invalid channel ID received from Slack API', {
channelId: channel.id,
channelName: channel.name,
error: validation.error,
})
return false
}
if (!/^[CDG][A-Z0-9]+$/i.test(channel.id)) {
logger.warn('Channel ID does not match Slack format', {
channelId: channel.id,
channelName: channel.name,
})
return false
}
return true
})
.map((channel: SlackChannel) => ({
id: channel.id,
name: channel.name,

View File

@@ -14,7 +14,12 @@ const SlackReadMessagesSchema = z
accessToken: z.string().min(1, 'Access token is required'),
channel: z.string().optional().nullable(),
userId: z.string().optional().nullable(),
limit: z.number().optional().nullable(),
limit: z.coerce
.number()
.min(1, 'Limit must be at least 1')
.max(15, 'Limit cannot exceed 15')
.optional()
.nullable(),
oldest: z.string().optional().nullable(),
latest: z.string().optional().nullable(),
})
@@ -62,8 +67,8 @@ export async function POST(request: NextRequest) {
const url = new URL('https://slack.com/api/conversations.history')
url.searchParams.append('channel', channel!)
const limit = validatedData.limit ? Number(validatedData.limit) : 10
url.searchParams.append('limit', String(Math.min(limit, 15)))
const limit = validatedData.limit ?? 10
url.searchParams.append('limit', String(limit))
if (validatedData.oldest) {
url.searchParams.append('oldest', validatedData.oldest)

View File

@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -20,13 +21,21 @@ export async function POST(request: Request) {
try {
const requestId = generateRequestId()
const body = await request.json()
const { credential, workflowId } = body
const { credential, workflowId, userId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (userId !== undefined && userId !== null) {
const validation = validateAlphanumericId(userId, 'userId', 100)
if (!validation.isValid) {
logger.warn('Invalid Slack user ID', { userId, error: validation.error })
return NextResponse.json({ error: validation.error }, { status: 400 })
}
}
let accessToken: string
const isBotToken = credential.startsWith('xoxb-')
@@ -63,6 +72,17 @@ export async function POST(request: Request) {
logger.info('Using OAuth token for Slack API')
}
if (userId) {
const userData = await fetchSlackUser(accessToken, userId)
const user = {
id: userData.user.id,
name: userData.user.name,
real_name: userData.user.real_name || userData.user.name,
}
logger.info(`Successfully fetched Slack user: ${userId}`)
return NextResponse.json({ user })
}
const data = await fetchSlackUsers(accessToken)
const users = (data.members || [])
@@ -87,6 +107,31 @@ export async function POST(request: Request) {
}
}
async function fetchSlackUser(accessToken: string, userId: string) {
const url = new URL('https://slack.com/api/users.info')
url.searchParams.append('user', userId)
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`Slack API error: ${response.status} ${response.statusText}`)
}
const data = await response.json()
if (!data.ok) {
throw new Error(data.error || 'Failed to fetch user')
}
return data
}
async function fetchSlackUsers(accessToken: string) {
const url = new URL('https://slack.com/api/users.list')
url.searchParams.append('limit', '200')

View File

@@ -1,4 +1,7 @@
import { type Attributes, Client, type ConnectConfig } from 'ssh2'
import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('SSHUtils')
// File type constants from POSIX
const S_IFMT = 0o170000 // bit mask for the file type bit field
@@ -32,7 +35,6 @@ function formatSSHError(err: Error, config: { host: string; port: number }): Err
const host = config.host
const port = config.port
// Connection refused - server not running or wrong port
if (errorMessage.includes('econnrefused') || errorMessage.includes('connection refused')) {
return new Error(
`Connection refused to ${host}:${port}. ` +
@@ -42,7 +44,6 @@ function formatSSHError(err: Error, config: { host: string; port: number }): Err
)
}
// Connection reset - server closed connection unexpectedly
if (errorMessage.includes('econnreset') || errorMessage.includes('connection reset')) {
return new Error(
`Connection reset by ${host}:${port}. ` +
@@ -53,7 +54,6 @@ function formatSSHError(err: Error, config: { host: string; port: number }): Err
)
}
// Timeout - server unreachable or slow
if (errorMessage.includes('etimedout') || errorMessage.includes('timeout')) {
return new Error(
`Connection timed out to ${host}:${port}. ` +
@@ -63,7 +63,6 @@ function formatSSHError(err: Error, config: { host: string; port: number }): Err
)
}
// DNS/hostname resolution
if (errorMessage.includes('enotfound') || errorMessage.includes('getaddrinfo')) {
return new Error(
`Could not resolve hostname "${host}". ` +
@@ -71,7 +70,6 @@ function formatSSHError(err: Error, config: { host: string; port: number }): Err
)
}
// Authentication failure
if (errorMessage.includes('authentication') || errorMessage.includes('auth')) {
return new Error(
`Authentication failed for user on ${host}:${port}. ` +
@@ -81,7 +79,6 @@ function formatSSHError(err: Error, config: { host: string; port: number }): Err
)
}
// Private key format issues
if (
errorMessage.includes('key') &&
(errorMessage.includes('parse') || errorMessage.includes('invalid'))
@@ -93,7 +90,6 @@ function formatSSHError(err: Error, config: { host: string; port: number }): Err
)
}
// Host key verification (first connection)
if (errorMessage.includes('host key') || errorMessage.includes('hostkey')) {
return new Error(
`Host key verification issue for ${host}. ` +
@@ -101,7 +97,6 @@ function formatSSHError(err: Error, config: { host: string; port: number }): Err
)
}
// Return original error with context if no specific match
return new Error(`SSH connection to ${host}:${port} failed: ${err.message}`)
}
@@ -205,19 +200,119 @@ export function executeSSHCommand(client: Client, command: string): Promise<SSHC
/**
* Sanitize command input to prevent command injection
*
* Removes null bytes and other dangerous control characters while preserving
* legitimate shell syntax. Logs warnings for potentially dangerous patterns.
*
* Note: This function does not block complex shell commands (pipes, redirects, etc.)
* as users legitimately need these features for remote command execution.
*
* @param command - The command to sanitize
* @returns The sanitized command string
*
* @example
* ```typescript
* const safeCommand = sanitizeCommand(userInput)
* // Use safeCommand for SSH execution
* ```
*/
export function sanitizeCommand(command: string): string {
return command.trim()
let sanitized = command.replace(/\0/g, '')
sanitized = sanitized.replace(/[\x0B\x0C]/g, '')
sanitized = sanitized.trim()
const dangerousPatterns = [
{ pattern: /\$\(.*\)/, name: 'command substitution $()' },
{ pattern: /`.*`/, name: 'backtick command substitution' },
{ pattern: /;\s*rm\s+-rf/i, name: 'destructive rm -rf command' },
{ pattern: /;\s*dd\s+/i, name: 'dd command (disk operations)' },
{ pattern: /mkfs/i, name: 'filesystem formatting command' },
{ pattern: />\s*\/dev\/sd[a-z]/i, name: 'direct disk write' },
]
for (const { pattern, name } of dangerousPatterns) {
if (pattern.test(sanitized)) {
logger.warn(`Command contains ${name}`, {
command: sanitized.substring(0, 100) + (sanitized.length > 100 ? '...' : ''),
})
}
}
return sanitized
}
/**
* Sanitize file path - removes null bytes and trims whitespace
* Sanitize and validate file path to prevent path traversal attacks
*
* This function validates that a file path does not contain:
* - Null bytes
* - Path traversal sequences (.. or ../)
* - URL-encoded path traversal attempts
*
* @param path - The file path to sanitize and validate
* @returns The sanitized path if valid
* @throws Error if path traversal is detected
*
* @example
* ```typescript
* try {
* const safePath = sanitizePath(userInput)
* // Use safePath safely
* } catch (error) {
* // Handle invalid path
* }
* ```
*/
export function sanitizePath(path: string): string {
let sanitized = path.replace(/\0/g, '')
sanitized = sanitized.trim()
if (sanitized.includes('%00')) {
logger.warn('Path contains URL-encoded null bytes', {
path: path.substring(0, 100),
})
throw new Error('Path contains invalid characters')
}
const pathTraversalPatterns = [
'../', // Standard Unix path traversal
'..\\', // Windows path traversal
'/../', // Mid-path traversal
'\\..\\', // Windows mid-path traversal
'%2e%2e%2f', // Fully encoded ../
'%2e%2e/', // Partially encoded ../
'%2e%2e%5c', // Fully encoded ..\
'%2e%2e\\', // Partially encoded ..\
'..%2f', // .. with encoded /
'..%5c', // .. with encoded \
'%252e%252e', // Double URL encoded ..
'..%252f', // .. with double encoded /
'..%255c', // .. with double encoded \
]
const lowerPath = sanitized.toLowerCase()
for (const pattern of pathTraversalPatterns) {
if (lowerPath.includes(pattern.toLowerCase())) {
logger.warn('Path traversal attempt detected', {
pattern,
path: path.substring(0, 100),
})
throw new Error('Path contains invalid path traversal sequences')
}
}
const segments = sanitized.split(/[/\\]/)
for (const segment of segments) {
if (segment === '..') {
logger.warn('Path traversal attempt detected (.. as path segment)', {
path: path.substring(0, 100),
})
throw new Error('Path contains invalid path traversal sequences')
}
}
return sanitized
}

View File

@@ -3,6 +3,7 @@ import { account } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateEnum, validatePathSegment } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -11,7 +12,6 @@ export const dynamic = 'force-dynamic'
const logger = createLogger('WealthboxItemsAPI')
// Interface for transformed Wealthbox items
interface WealthboxItem {
id: string
name: string
@@ -45,12 +45,23 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
if (type !== 'contact') {
const credentialIdValidation = validatePathSegment(credentialId, {
paramName: 'credentialId',
maxLength: 100,
allowHyphens: true,
allowUnderscores: true,
allowDots: false,
})
if (!credentialIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid credentialId format: ${credentialId}`)
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
}
const ALLOWED_TYPES = ['contact'] as const
const typeValidation = validateEnum(type, ALLOWED_TYPES, 'type')
if (!typeValidation.isValid) {
logger.warn(`[${requestId}] Invalid item type: ${type}`)
return NextResponse.json(
{ error: 'Invalid item type. Only contact is supported.' },
{ status: 400 }
)
return NextResponse.json({ error: typeValidation.error }, { status: 400 })
}
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)

View File

@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -12,13 +13,21 @@ export async function POST(request: Request) {
try {
const requestId = generateRequestId()
const body = await request.json()
const { credential, workflowId } = body
const { credential, workflowId, siteId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (siteId) {
const siteIdValidation = validateAlphanumericId(siteId, 'siteId')
if (!siteIdValidation.isValid) {
logger.error('Invalid siteId', { error: siteIdValidation.error })
return NextResponse.json({ error: siteIdValidation.error }, { status: 400 })
}
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
@@ -46,7 +55,11 @@ export async function POST(request: Request) {
)
}
const response = await fetch('https://api.webflow.com/v2/sites', {
const url = siteId
? `https://api.webflow.com/v2/sites/${siteId}`
: 'https://api.webflow.com/v2/sites'
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
accept: 'application/json',
@@ -58,6 +71,7 @@ export async function POST(request: Request) {
logger.error('Failed to fetch Webflow sites', {
status: response.status,
error: errorData,
siteId: siteId || 'all',
})
return NextResponse.json(
{ error: 'Failed to fetch Webflow sites', details: errorData },
@@ -66,7 +80,13 @@ export async function POST(request: Request) {
}
const data = await response.json()
const sites = data.sites || []
let sites: any[]
if (siteId) {
sites = [data]
} else {
sites = data.sites || []
}
const formattedSites = sites.map((site: any) => ({
id: site.id,

View File

@@ -32,7 +32,6 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ error: 'Missing email or token parameter' }, { status: 400 })
}
// Verify token and get email type
const tokenVerification = verifyUnsubscribeToken(email, token)
if (!tokenVerification.valid) {
logger.warn(`[${requestId}] Invalid unsubscribe token for email: ${email}`)
@@ -42,7 +41,6 @@ export async function GET(req: NextRequest) {
const emailType = tokenVerification.emailType as EmailType
const isTransactional = isTransactionalEmail(emailType)
// Get current preferences
const preferences = await getEmailPreferences(email)
logger.info(
@@ -67,22 +65,42 @@ export async function POST(req: NextRequest) {
const requestId = generateRequestId()
try {
const body = await req.json()
const result = unsubscribeSchema.safeParse(body)
const { searchParams } = new URL(req.url)
const contentType = req.headers.get('content-type') || ''
if (!result.success) {
logger.warn(`[${requestId}] Invalid unsubscribe POST data`, {
errors: result.error.format(),
})
return NextResponse.json(
{ error: 'Invalid request data', details: result.error.format() },
{ status: 400 }
)
let email: string
let token: string
let type: 'all' | 'marketing' | 'updates' | 'notifications' = 'all'
if (contentType.includes('application/x-www-form-urlencoded')) {
email = searchParams.get('email') || ''
token = searchParams.get('token') || ''
if (!email || !token) {
logger.warn(`[${requestId}] One-click unsubscribe missing email or token in URL`)
return NextResponse.json({ error: 'Missing email or token parameter' }, { status: 400 })
}
logger.info(`[${requestId}] Processing one-click unsubscribe for: ${email}`)
} else {
const body = await req.json()
const result = unsubscribeSchema.safeParse(body)
if (!result.success) {
logger.warn(`[${requestId}] Invalid unsubscribe POST data`, {
errors: result.error.format(),
})
return NextResponse.json(
{ error: 'Invalid request data', details: result.error.format() },
{ status: 400 }
)
}
email = result.data.email
token = result.data.token
type = result.data.type
}
const { email, token, type } = result.data
// Verify token and get email type
const tokenVerification = verifyUnsubscribeToken(email, token)
if (!tokenVerification.valid) {
logger.warn(`[${requestId}] Invalid unsubscribe token for email: ${email}`)
@@ -92,7 +110,6 @@ export async function POST(req: NextRequest) {
const emailType = tokenVerification.emailType as EmailType
const isTransactional = isTransactionalEmail(emailType)
// Prevent unsubscribing from transactional emails
if (isTransactional) {
logger.warn(`[${requestId}] Attempted to unsubscribe from transactional email: ${email}`)
return NextResponse.json(
@@ -106,7 +123,6 @@ export async function POST(req: NextRequest) {
)
}
// Process unsubscribe based on type
let success = false
switch (type) {
case 'all':
@@ -130,7 +146,6 @@ export async function POST(req: NextRequest) {
logger.info(`[${requestId}] Successfully unsubscribed ${email} from ${type}`)
// Return 200 for one-click unsubscribe compliance
return NextResponse.json(
{
success: true,

View File

@@ -11,6 +11,7 @@ import { processInputFileFields } from '@/lib/execution/files'
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { createLogger } from '@/lib/logs/console/logger'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
import { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events'
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
@@ -30,7 +31,7 @@ const logger = createLogger('WorkflowExecuteAPI')
const ExecuteWorkflowSchema = z.object({
selectedOutputs: z.array(z.string()).optional().default([]),
triggerType: z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']).optional(),
triggerType: z.enum(ALL_TRIGGER_TYPES).optional(),
stream: z.boolean().optional(),
useDraftState: z.boolean().optional(),
input: z.any().optional(),

View File

@@ -1,97 +0,0 @@
import { db } from '@sim/db'
import { userStats, workflow } from '@sim/db/schema'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('WorkflowStatsAPI')
const queryParamsSchema = z.object({
runs: z.coerce.number().int().min(1).max(100).default(1),
})
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const searchParams = request.nextUrl.searchParams
const validation = queryParamsSchema.safeParse({
runs: searchParams.get('runs'),
})
if (!validation.success) {
logger.error(`Invalid query parameters: ${validation.error.message}`)
return NextResponse.json(
{
error:
validation.error.errors[0]?.message ||
'Invalid number of runs. Must be between 1 and 100.',
},
{ status: 400 }
)
}
const { runs } = validation.data
try {
const [workflowRecord] = await db.select().from(workflow).where(eq(workflow.id, id)).limit(1)
if (!workflowRecord) {
return NextResponse.json({ error: `Workflow ${id} not found` }, { status: 404 })
}
try {
await db
.update(workflow)
.set({
runCount: workflowRecord.runCount + runs,
lastRunAt: new Date(),
})
.where(eq(workflow.id, id))
} catch (error) {
logger.error('Error updating workflow runCount:', error)
throw error
}
try {
const userStatsRecords = await db
.select()
.from(userStats)
.where(eq(userStats.userId, workflowRecord.userId))
if (userStatsRecords.length === 0) {
await db.insert(userStats).values({
id: crypto.randomUUID(),
userId: workflowRecord.userId,
totalManualExecutions: 0,
totalApiCalls: 0,
totalWebhookTriggers: 0,
totalScheduledExecutions: 0,
totalChatExecutions: 0,
totalTokensUsed: 0,
totalCost: '0.00',
lastActive: sql`now()`,
})
} else {
await db
.update(userStats)
.set({
lastActive: sql`now()`,
})
.where(eq(userStats.userId, workflowRecord.userId))
}
} catch (error) {
logger.error(`Error ensuring userStats for userId ${workflowRecord.userId}:`, error)
// Don't rethrow - we want to continue even if this fails
}
return NextResponse.json({
success: true,
runsAdded: runs,
newTotal: workflowRecord.runCount + runs,
})
} catch (error) {
logger.error('Error updating workflow stats:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -6,13 +6,14 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { createLogger } from '@/lib/logs/console/logger'
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
import { MAX_EMAIL_RECIPIENTS, MAX_WORKFLOW_IDS } from '../constants'
const logger = createLogger('WorkspaceNotificationAPI')
const levelFilterSchema = z.array(z.enum(['info', 'error']))
const triggerFilterSchema = z.array(z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']))
const triggerFilterSchema = z.array(z.enum(ALL_TRIGGER_TYPES))
const alertRuleSchema = z.enum([
'consecutive_failures',

View File

@@ -7,6 +7,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { createLogger } from '@/lib/logs/console/logger'
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
import { MAX_EMAIL_RECIPIENTS, MAX_NOTIFICATIONS_PER_TYPE, MAX_WORKFLOW_IDS } from './constants'
@@ -14,7 +15,7 @@ const logger = createLogger('WorkspaceNotificationsAPI')
const notificationTypeSchema = z.enum(['webhook', 'email', 'slack'])
const levelFilterSchema = z.array(z.enum(['info', 'error']))
const triggerFilterSchema = z.array(z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']))
const triggerFilterSchema = z.array(z.enum(ALL_TRIGGER_TYPES))
const alertRuleSchema = z.enum([
'consecutive_failures',
@@ -80,7 +81,7 @@ const createNotificationSchema = z
workflowIds: z.array(z.string()).max(MAX_WORKFLOW_IDS).default([]),
allWorkflows: z.boolean().default(false),
levelFilter: levelFilterSchema.default(['info', 'error']),
triggerFilter: triggerFilterSchema.default(['api', 'webhook', 'schedule', 'manual', 'chat']),
triggerFilter: triggerFilterSchema.default([...ALL_TRIGGER_TYPES]),
includeFinalOutput: z.boolean().default(false),
includeTraceSpans: z.boolean().default(false),
includeRateLimits: z.boolean().default(false),

View File

@@ -173,7 +173,7 @@ export async function GET(
// DELETE /api/workspaces/invitations/[invitationId] - Delete a workspace invitation
export async function DELETE(
_req: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ invitationId: string }> }
) {
const { invitationId } = await params
@@ -221,7 +221,7 @@ export async function DELETE(
// POST /api/workspaces/invitations/[invitationId] - Resend a workspace invitation
export async function POST(
_req: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ invitationId: string }> }
) {
const { invitationId } = await params

View File

@@ -29,30 +29,24 @@ export const metadata: Metadata = {
locale: 'en_US',
images: [
{
url: '/social/og-image.png',
width: 1200,
height: 630,
alt: 'Sim - Visual AI Workflow Builder',
url: '/logo/primary/rounded.png',
width: 512,
height: 512,
alt: 'Sim - AI Agent Workflow Builder',
type: 'image/png',
},
{
url: '/social/og-image-square.png',
width: 600,
height: 600,
alt: 'Sim Logo',
},
],
},
twitter: {
card: 'summary_large_image',
card: 'summary',
site: '@simdotai',
creator: '@simdotai',
title: 'Sim - AI Agent Workflow Builder | Open Source',
description:
'Open-source platform for agentic workflows. 60,000+ developers. Visual builder. 100+ integrations. SOC2 & HIPAA compliant.',
images: {
url: '/social/twitter-image.png',
alt: 'Sim - Visual AI Workflow Builder',
url: '/logo/primary/rounded.png',
alt: 'Sim - AI Agent Workflow Builder',
},
},
alternates: {
@@ -77,7 +71,6 @@ export const metadata: Metadata = {
category: 'technology',
classification: 'AI Development Tools',
referrer: 'origin-when-cross-origin',
// LLM SEO optimizations
other: {
'llm:content-type': 'AI workflow builder, visual programming, no-code AI development',
'llm:use-cases':

View File

@@ -1,5 +1,88 @@
import { db } from '@sim/db'
import { templateCreators, templates } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import type { Metadata } from 'next'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
import TemplateDetails from '@/app/templates/[id]/template'
const logger = createLogger('TemplateMetadata')
/**
* Generate dynamic metadata for template pages.
* This provides OpenGraph images for social media sharing.
*/
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await params
try {
const result = await db
.select({
template: templates,
creator: templateCreators,
})
.from(templates)
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
.where(eq(templates.id, id))
.limit(1)
if (result.length === 0) {
return {
title: 'Template Not Found',
description: 'The requested template could not be found.',
}
}
const { template, creator } = result[0]
const baseUrl = getBaseUrl()
const details = template.details as { tagline?: string; about?: string } | null
const description = details?.tagline || 'AI workflow template on Sim'
const hasOgImage = !!template.ogImageUrl
const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
return {
title: template.name,
description,
openGraph: {
title: template.name,
description,
type: 'website',
url: `${baseUrl}/templates/${id}`,
siteName: 'Sim',
images: [
{
url: ogImageUrl,
width: hasOgImage ? 1200 : 512,
height: hasOgImage ? 630 : 512,
alt: `${template.name} - Workflow Preview`,
},
],
},
twitter: {
card: hasOgImage ? 'summary_large_image' : 'summary',
title: template.name,
description,
images: [ogImageUrl],
creator: creator?.details
? ((creator.details as Record<string, unknown>).xHandle as string) || undefined
: undefined,
},
}
} catch (error) {
logger.error('Failed to generate template metadata:', error)
return {
title: 'Template',
description: 'AI workflow template on Sim',
}
}
}
/**
* Public template detail page for unauthenticated users.
* Authenticated-user redirect is handled in templates/[id]/layout.tsx.

Some files were not shown because too many files have changed in this diff Show More