mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
Compare commits
88 Commits
improvemen
...
v0.5.101
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fd0989264 | ||
|
|
345a95f48d | ||
|
|
e07963f88c | ||
|
|
25c59e3e2e | ||
|
|
dde098e8e5 | ||
|
|
5ae0115444 | ||
|
|
fbafe204e5 | ||
|
|
ba7d6ff298 | ||
|
|
40016e79a1 | ||
|
|
e4fb8b2fdd | ||
|
|
d98545d554 | ||
|
|
fadbad4085 | ||
|
|
244e1ee495 | ||
|
|
1f3dc52d15 | ||
|
|
f625482bcb | ||
|
|
16f337f6fd | ||
|
|
063ec87ced | ||
|
|
870d4b55c6 | ||
|
|
95304b2941 | ||
|
|
8b0c47b06c | ||
|
|
774771fddd | ||
|
|
67f8a687f6 | ||
|
|
43c0f5b199 | ||
|
|
ff01825b20 | ||
|
|
58d0fda173 | ||
|
|
ecdb133d1b | ||
|
|
d06459f489 | ||
|
|
0574427d45 | ||
|
|
8f9b859a53 | ||
|
|
60f9eb21bf | ||
|
|
9a31c7d8ad | ||
|
|
9e817bc5b0 | ||
|
|
d824ce5b07 | ||
|
|
9bd357f184 | ||
|
|
d4a014f423 | ||
|
|
fe34d23a98 | ||
|
|
b8dfb4dd20 | ||
|
|
91666491cd | ||
|
|
eafbb9fef4 | ||
|
|
132fef06a1 | ||
|
|
2ae814549a | ||
|
|
e55d41f2ef | ||
|
|
364bb196ea | ||
|
|
af592349d3 | ||
|
|
69ec70af13 | ||
|
|
687c12528b | ||
|
|
996dc96d6e | ||
|
|
0d86ea01f0 | ||
|
|
04286fc16b | ||
|
|
c52f78c840 | ||
|
|
e318bf2e65 | ||
|
|
4913799a27 | ||
|
|
ccb4f5956d | ||
|
|
2a6d4fcb96 | ||
|
|
115f04e989 | ||
|
|
42020c3ae2 | ||
|
|
34d92fae89 | ||
|
|
a98463a486 | ||
|
|
765a481864 | ||
|
|
a1400caea0 | ||
|
|
2fc2e12cb2 | ||
|
|
3fa4bb4c12 | ||
|
|
67aa4bb332 | ||
|
|
1b8d666c93 | ||
|
|
71942cb53c | ||
|
|
12534163c1 | ||
|
|
55920e9b03 | ||
|
|
958dd64740 | ||
|
|
68f44b8df4 | ||
|
|
9920882dc5 | ||
|
|
9ca5254c2b | ||
|
|
d7fddb2909 | ||
|
|
61c7afc19e | ||
|
|
3c470ab0f8 | ||
|
|
2b5e436a2a | ||
|
|
e24c824c9a | ||
|
|
dcf81372af | ||
|
|
15ace5e63f | ||
|
|
ab48787422 | ||
|
|
91aa1f9a52 | ||
|
|
2979269ac3 | ||
|
|
cf28822a1c | ||
|
|
fdca73679d | ||
|
|
86ca984926 | ||
|
|
e3964624ac | ||
|
|
7c7c0fd955 | ||
|
|
da46a387c9 | ||
|
|
b7e377ec4b |
@@ -454,6 +454,8 @@ Enables AI-assisted field generation.
|
||||
|
||||
## Tools Configuration
|
||||
|
||||
**Important:** `tools.config.tool` runs during serialization before variable resolution. Put `Number()` and other type coercions in `tools.config.params` instead, which runs at execution time after variables are resolved.
|
||||
|
||||
**Preferred:** Use tool names directly as dropdown option IDs to avoid switch cases:
|
||||
```typescript
|
||||
// Dropdown options use tool IDs directly
|
||||
|
||||
2
.github/workflows/images.yml
vendored
2
.github/workflows/images.yml
vendored
@@ -146,7 +146,7 @@ jobs:
|
||||
|
||||
create-ghcr-manifests:
|
||||
name: Create GHCR Manifests
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
needs: [build-amd64, build-ghcr-arm64]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
strategy:
|
||||
|
||||
2
.github/workflows/test-build.yml
vendored
2
.github/workflows/test-build.yml
vendored
@@ -110,7 +110,7 @@ jobs:
|
||||
RESEND_API_KEY: 'dummy_key_for_ci_only'
|
||||
AWS_REGION: 'us-west-2'
|
||||
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
|
||||
run: bun run build
|
||||
run: bunx turbo run build --filter=sim
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -73,3 +73,7 @@ start-collector.sh
|
||||
## Helm Chart Tests
|
||||
helm/sim/test
|
||||
i18n.cache
|
||||
|
||||
## Claude Code
|
||||
.claude/launch.json
|
||||
.claude/worktrees/
|
||||
|
||||
@@ -238,7 +238,7 @@ export const ServiceBlock: BlockConfig = {
|
||||
bgColor: '#hexcolor',
|
||||
icon: ServiceIcon,
|
||||
subBlocks: [ /* see SubBlock Properties */ ],
|
||||
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}` } },
|
||||
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}`, params: (p) => ({ /* type coercions here */ }) } },
|
||||
inputs: { /* ... */ },
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
@@ -246,6 +246,8 @@ export const ServiceBlock: BlockConfig = {
|
||||
|
||||
Register in `blocks/registry.ts` (alphabetically).
|
||||
|
||||
**Important:** `tools.config.tool` runs during serialization (before variable resolution). Never do `Number()` or other type coercions there — dynamic references like `<Block.output>` will be destroyed. Use `tools.config.params` for type coercions (it runs during execution, after variables are resolved).
|
||||
|
||||
**SubBlock Properties:**
|
||||
```typescript
|
||||
{
|
||||
|
||||
@@ -710,6 +710,17 @@ export function NotionIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GongIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 55.4 60' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M54.1,25.7H37.8c-0.9,0-1.6,1-1.3,1.8l3.9,10.1c0.2,0.4-0.2,0.9-0.7,0.9l-5-0.3c-0.2,0-0.4,0.1-0.6,0.3L30.3,44c-0.2,0.3-0.6,0.4-1,0.2l-5.8-3.9c-0.2-0.2-0.5-0.2-0.8,0l-8,5.4c-0.5,0.4-1.2-0.1-1-0.7L16,37c0.1-0.3-0.1-0.7-0.4-0.8l-4.2-1.7c-0.4-0.2-0.6-0.7-0.3-1l3.7-4.6c0.2-0.2,0.2-0.6,0-0.8l-3.1-4.5c-0.3-0.4,0-1,0.5-1l4.9-0.4c0.4,0,0.6-0.3,0.6-0.7l-0.4-6.8c0-0.5,0.5-0.8,0.9-0.7l6,2.5c0.3,0.1,0.6,0,0.8-0.2l4.2-4.6c0.3-0.4,0.9-0.3,1.1,0.2l2.5,6.4c0.3,0.8,1.3,1.1,2,0.6l9.8-7.3c1.1-0.8,0.4-2.6-1-2.4L37.3,10c-0.3,0-0.6-0.1-0.7-0.4l-3.4-8.7c-0.4-0.9-1.5-1.1-2.2-0.4l-7.4,8c-0.2,0.2-0.5,0.3-0.8,0.2l-9.7-4.1c-0.9-0.4-1.8,0.2-1.9,1.2l-0.4,10c0,0.4-0.3,0.6-0.6,0.6l-8.9,0.6c-1,0.1-1.6,1.2-1,2.1l5.9,8.7c0.2,0.2,0.2,0.6,0,0.8l-6,6.9C-0.3,36,0,37.1,0.8,37.4l6.9,3c0.3,0.1,0.5,0.5,0.4,0.8L3.7,58.3c-0.3,1.2,1.1,2.1,2.1,1.4l16.5-11.8c0.2-0.2,0.5-0.2,0.8,0l7.5,5.3c0.6,0.4,1.5,0.3,1.9-0.4l4.7-7.2c0.1-0.2,0.4-0.3,0.6-0.3l11.2,1.4c0.9,0.1,1.8-0.6,1.5-1.5l-4.7-12.1c-0.1-0.3,0-0.7,0.4-0.9l8.5-4C55.9,27.6,55.5,25.7,54.1,25.7z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GmailIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -928,6 +939,25 @@ export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DevinIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 500 500' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M59.29,209.39l48.87,28.21c1.75,1.01,3.71,1.51,5.67,1.51c1.95,0,3.92-0.52,5.67-1.51l48.87-28.21c0,0,0.14-0.11,0.2-0.16c0.74-0.45,1.44-0.99,2.07-1.6c0.09-0.09,0.18-0.2,0.27-0.29c0.54-0.58,1.03-1.21,1.44-1.89c0.06-0.11,0.16-0.2,0.2-0.32c0.43-0.74,0.74-1.53,0.99-2.37c0.05-0.18,0.09-0.36,0.14-0.54c0.2-0.86,0.36-1.74,0.36-2.66v-28.21c0-10.89,5.87-21.03,15.3-26.48c9.42-5.45,21.15-5.44,30.59,0l24.43,14.11c0.79,0.45,1.62,0.77,2.47,1.01c0.18,0.05,0.37,0.11,0.54,0.16c0.83,0.2,1.69,0.32,2.54,0.34c0.05,0,0.09,0,0.11,0c0.09,0,0.18-0.05,0.26-0.05c0.79,0,1.58-0.11,2.34-0.32c0.14-0.03,0.27-0.05,0.4-0.09c0.83-0.23,1.64-0.57,2.41-0.99c0.06-0.05,0.16-0.05,0.23-0.09l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81V64.52c0-4.05-2.16-7.78-5.67-9.81l-48.91-28.19c-3.51-2.03-7.81-2.03-11.32,0l-48.87,28.21c0,0-0.14,0.11-0.2,0.16c-0.74,0.45-1.44,0.99-2.07,1.6c-0.09,0.09-0.18,0.2-0.27,0.29c-0.54,0.58-1.03,1.21-1.44,1.89c-0.06,0.11-0.16,0.2-0.2,0.31c-0.43,0.74-0.74,1.53-0.99,2.37c-0.05,0.18-0.09,0.36-0.14,0.54c-0.2,0.86-0.36,1.74-0.36,2.66v28.21c0,10.89-5.87,21.03-15.3,26.5c-9.42,5.44-21.15,5.44-30.59,0l-24.42-14.1c-0.79-0.45-1.63-0.77-2.47-1.01c-0.18-0.05-0.36-0.11-0.54-0.16c-0.84-0.2-1.69-0.31-2.55-0.34c-0.14,0-0.25,0-0.38,0c-0.81,0-1.6,0.11-2.37,0.31c-0.14,0.02-0.25,0.05-0.38,0.09c-0.82,0.23-1.63,0.57-2.4,1c-0.06,0.05-0.16,0.05-0.23,0.09l-48.84,28.24c-3.51,2.03-5.67,5.76-5.67,9.81v56.42c0,4.05,2.16,7.78,5.67,9.81C59.29,209.41,59.29,209.39,59.29,209.39z'
|
||||
fill='#2A6DCE'
|
||||
/>
|
||||
<path
|
||||
d='M325.46,223.49c9.42-5.44,21.15-5.44,30.59,0l24.43,14.11c0.79,0.45,1.62,0.77,2.47,1.01c0.18,0.05,0.36,0.11,0.54,0.16c0.83,0.2,1.69,0.31,2.54,0.34c0.05,0,0.09,0,0.11,0c0.09,0,0.18-0.03,0.26-0.05c0.79,0,1.58-0.11,2.34-0.31c0.14-0.03,0.27-0.05,0.4-0.09c0.83-0.23,1.62-0.57,2.41-0.99c0.06-0.05,0.16-0.05,0.25-0.09l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81l-48.84-28.22c-3.51-2.03-7.81-2.03-11.32,0l-48.87,28.21c0,0-0.14,0.11-0.2,0.16c-0.74,0.45-1.44,0.99-2.07,1.6c-0.09,0.09-0.18,0.2-0.26,0.29c-0.54,0.58-1.03,1.21-1.44,1.89c-0.06,0.11-0.16,0.2-0.2,0.32c-0.43,0.74-0.74,1.53-0.99,2.37c-0.05,0.18-0.09,0.36-0.14,0.54c-0.2,0.86-0.36,1.74-0.36,2.66v28.21c0,10.89-5.87,21.03-15.3,26.5c-9.42,5.44-21.15,5.44-30.59,0l-24.43-14.11c-0.79-0.45-1.62-0.77-2.47-1.01c-0.18-0.05-0.36-0.11-0.54-0.16c-0.83-0.2-1.69-0.32-2.54-0.34c-0.14,0-0.25,0-0.38,0c-0.81,0-1.6,0.11-2.37,0.32c-0.14,0.03-0.25,0.05-0.38,0.09c-0.83,0.23-1.64,0.57-2.41,0.99c-0.06,0.05-0.16,0.05-0.23,0.09l-48.87,28.21c-3.51,2.03-5.67,5.76-5.67,9.81v56.43c0,4.05,2.16,7.78,5.67,9.81l48.87,28.21c0,0,0.16,0.05,0.23,0.09c0.77,0.43,1.58,0.77,2.41,0.99c0.14,0.05,0.27,0.05,0.4,0.09c0.77,0.18,1.55,0.29,2.34,0.32c0.09,0,0.18,0.05,0.27,0.05c0.05,0,0.09,0,0.11,0c0.86,0,1.69-0.14,2.54-0.34c0.18-0.05,0.36-0.09,0.54-0.16c0.86-0.25,1.69-0.57,2.47-1.01l24.43-14.11c9.42-5.44,21.15-5.44,30.59,0c9.42,5.44,15.3,15.59,15.3,26.48v28.21c0,0.92,0.14,1.8,0.36,2.66c0.05,0.18,0.09,0.36,0.14,0.54c0.25,0.83,0.56,1.62,0.99,2.37c0.06,0.11,0.14,0.2,0.2,0.31c0.4,0.68,0.9,1.31,1.44,1.89c0.09,0.09,0.18,0.2,0.26,0.29c0.61,0.6,1.31,1.12,2.07,1.6c0.06,0.05,0.11,0.11,0.2,0.16l48.87,28.21c1.75,1.01,3.72,1.51,5.67,1.51s3.92-0.52,5.67-1.51l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81l-48.87-28.21c0,0-0.16-0.05-0.23-0.09c-0.77-0.43-1.58-0.77-2.41-0.99c-0.14-0.05-0.25-0.05-0.38-0.09c-0.79-0.18-1.57-0.29-2.38-0.32c-0.11,0-0.25,0-0.36,0c-0.86,0-1.71,0.14-2.54,0.34c-0.18,0.05-0.34,0.09-0.52,0.16c-0.86,0.25-1.69,0.57-2.47,1.01l-24.43,14.11c-9.42,5.44-21.15,5.44-30.58,0c-9.42-5.44-15.3-15.59-15.3-26.5c0-10.91,5.87-21.03,15.3-26.48C325.55,223.49,325.46,223.49,325.46,223.49z'
|
||||
fill='#1DC19C'
|
||||
/>
|
||||
<path
|
||||
d='M304.5,369.22l-48.87-28.21c0,0-0.16-0.05-0.23-0.09c-0.77-0.43-1.57-0.77-2.41-0.99c-0.14-0.05-0.27-0.05-0.4-0.09c-0.79-0.18-1.57-0.29-2.37-0.32c-0.14,0-0.25,0-0.38,0c-0.86,0-1.71,0.14-2.54,0.34c-0.18,0.05-0.34,0.09-0.52,0.16c-0.86,0.25-1.69,0.57-2.47,1.01l-24.43,14.11c-9.42,5.44-21.15,5.44-30.58,0c-9.42-5.44-15.3-15.59-15.3-26.5v-28.22c0-0.92-0.14-1.8-0.36-2.66c-0.05-0.18-0.09-0.36-0.14-0.54c-0.25-0.83-0.57-1.62-0.99-2.37c-0.06-0.11-0.14-0.2-0.2-0.32c-0.4-0.68-0.9-1.31-1.44-1.89c-0.09-0.09-0.18-0.2-0.27-0.29c-0.6-0.6-1.31-1.12-2.07-1.6c-0.06-0.05-0.11-0.11-0.2-0.16l-48.87-28.21c-3.51-2.03-7.81-2.03-11.32,0L59.28,290.6c-3.51,2.03-5.67,5.76-5.67,9.81v56.43c0,4.05,2.16,7.78,5.67,9.81l48.87,28.21c0,0,0.16,0.06,0.23,0.09c0.77,0.43,1.55,0.77,2.38,0.99c0.14,0.05,0.27,0.06,0.4,0.09c0.77,0.18,1.55,0.29,2.34,0.32c0.09,0,0.18,0.05,0.29,0.05c0.05,0,0.09,0,0.14,0c0.86,0,1.69-0.14,2.52-0.34c0.18-0.05,0.36-0.09,0.54-0.16c0.86-0.25,1.69-0.57,2.47-1.01l24.43-14.11c9.42-5.44,21.15-5.44,30.59,0c9.42,5.44,15.3,15.59,15.3,26.48v28.21c0,0.92,0.14,1.8,0.36,2.66c0.05,0.18,0.09,0.36,0.14,0.54c0.25,0.83,0.57,1.62,0.99,2.37c0.06,0.11,0.14,0.2,0.2,0.32c0.4,0.68,0.9,1.31,1.44,1.89c0.09,0.09,0.18,0.2,0.27,0.29c0.61,0.61,1.31,1.12,2.07,1.6c0.06,0.05,0.11,0.11,0.2,0.16l48.87,28.21c1.75,1.01,3.71,1.51,5.67,1.51c1.96,0,3.92-0.52,5.67-1.51l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81L304.5,369.22z'
|
||||
fill='#1796E2'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1157,6 +1187,17 @@ export function AirweaveIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function AlgoliaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50'>
|
||||
<path
|
||||
fill='#FFFFFF'
|
||||
d='M25,0C11.3,0,0.2,11,0,24.6C-0.2,38.4,11,49.9,24.8,50c4.3,0,8.4-1,12-3c0.4-0.2,0.4-0.7,0.1-1l-2.3-2.1 c-0.5-0.4-1.2-0.5-1.7-0.3c-2.5,1.1-5.3,1.6-8.2,1.6c-11.2-0.1-20.2-9.4-20-20.6C4.9,13.6,13.9,4.7,25,4.7h20.3v36L33.7,30.5 c-0.4-0.3-0.9-0.3-1.2,0.1c-1.8,2.4-4.9,4-8.2,3.7c-4.6-0.3-8.4-4-8.7-8.7c-0.4-5.5,4-10.2,9.4-10.2c4.9,0,9,3.8,9.4,8.6 c0,0.4,0.2,0.8,0.6,1.1l3,2.7c0.3,0.3,0.9,0.1,1-0.3c0.2-1.2,0.3-2.4,0.2-3.6c-0.5-7-6.2-12.7-13.2-13.1c-8.1-0.5-14.8,5.8-15,13.7 c-0.2,7.7,6.1,14.4,13.8,14.5c3.2,0.1,6.2-0.9,8.6-2.7l15,13.3c0.6,0.6,1.7,0.1,1.7-0.7v-48C50,0.4,49.5,0,49,0L25,0 C25,0,25,0,25,0z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleBooksIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 478.633 540.068'>
|
||||
@@ -1280,6 +1321,21 @@ export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleTasksIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 527.1 500' xmlns='http://www.w3.org/2000/svg'>
|
||||
<polygon
|
||||
fill='#0066DA'
|
||||
points='410.4,58.3 368.8,81.2 348.2,120.6 368.8,168.8 407.8,211 450,187.5 475.9,142.8 450,87.5'
|
||||
/>
|
||||
<path
|
||||
fill='#2684FC'
|
||||
d='M249.3,219.4l98.9-98.9c29.1,22.1,50.5,53.8,59.6,90.4L272.1,346.7c-12.2,12.2-32,12.2-44.2,0l-91.5-91.5 c-9.8-9.8-9.8-25.6,0-35.3l39-39c9.8-9.8,25.6-9.8,35.3,0L249.3,219.4z M519.8,63.6l-39.7-39.7c-9.7-9.7-25.6-9.7-35.3,0 l-34.4,34.4c27.5,23,49.9,51.8,65.5,84.5l43.9-43.9C529.6,89.2,529.6,73.3,519.8,63.6z M412.5,250c0,89.8-72.8,162.5-162.5,162.5 S87.5,339.8,87.5,250S160.2,87.5,250,87.5c36.9,0,70.9,12.3,98.2,33.1l62.2-62.2C367,21.9,311.1,0,250,0C111.9,0,0,111.9,0,250 s111.9,250,250,250s250-111.9,250-250c0-38.3-8.7-74.7-24.1-107.2L407.8,211C410.8,223.5,412.5,236.6,412.5,250z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SupabaseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const gradient0 = `supabase_paint0_${id}`
|
||||
@@ -3408,6 +3464,23 @@ export const ResendIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleBigQueryIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
|
||||
<path
|
||||
d='M14.48 58.196L.558 34.082c-.744-1.288-.744-2.876 0-4.164L14.48 5.805c.743-1.287 2.115-2.08 3.6-2.082h27.857c1.48.007 2.845.8 3.585 2.082l13.92 24.113c.744 1.288.744 2.876 0 4.164L49.52 58.196c-.743 1.287-2.115 2.08-3.6 2.082H18.07c-1.483-.005-2.85-.798-3.593-2.082z'
|
||||
fill='#4386fa'
|
||||
/>
|
||||
<path
|
||||
d='M40.697 24.235s3.87 9.283-1.406 14.545-14.883 1.894-14.883 1.894L43.95 60.27h1.984c1.486-.002 2.858-.796 3.6-2.082L58.75 42.23z'
|
||||
opacity='.1'
|
||||
/>
|
||||
<path
|
||||
d='M45.267 43.23L41 38.953a.67.67 0 0 0-.158-.12 11.63 11.63 0 1 0-2.032 2.037.67.67 0 0 0 .113.15l4.277 4.277a.67.67 0 0 0 .947 0l1.12-1.12a.67.67 0 0 0 0-.947zM31.64 40.464a8.75 8.75 0 1 1 8.749-8.749 8.75 8.75 0 0 1-8.749 8.749zm-5.593-9.216v3.616c.557.983 1.363 1.803 2.338 2.375v-6.013zm4.375-2.998v9.772a6.45 6.45 0 0 0 2.338 0V28.25zm6.764 6.606v-2.142H34.85v4.5a6.43 6.43 0 0 0 2.338-2.368z'
|
||||
fill='#fff'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleVaultIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 82 82'>
|
||||
<path
|
||||
@@ -3530,6 +3603,15 @@ export function TrelloIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function AttioIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60.9 50' fill='currentColor'>
|
||||
<path d='M60.3,34.8l-5.1-8.1c0,0,0,0,0,0L54.7,26c-0.8-1.2-2.1-1.9-3.5-1.9L43,24L42.5,25l-9.8,15.7l-0.5,0.9l4.1,6.6c0.8,1.2,2.1,1.9,3.5,1.9h11.5c1.4,0,2.8-0.7,3.5-1.9l0.4-0.6c0,0,0,0,0,0l5.1-8.2C61.1,37.9,61.1,36.2,60.3,34.8L60.3,34.8z M58.7,38.3l-5.1,8.2c0,0,0,0.1-0.1,0.1c-0.2,0.2-0.4,0.2-0.5,0.2c-0.1,0-0.4,0-0.6-0.3l-5.1-8.2c-0.1-0.1-0.1-0.2-0.2-0.3c0-0.1-0.1-0.2-0.1-0.3c-0.1-0.4-0.1-0.8,0-1.3c0.1-0.2,0.1-0.4,0.3-0.6l5.1-8.1c0,0,0,0,0,0c0.1-0.2,0.3-0.3,0.4-0.3c0.1,0,0.1,0,0.1,0c0,0,0,0,0.1,0c0.1,0,0.4,0,0.6,0.3l5.1,8.1C59.2,36.6,59.2,37.5,58.7,38.3L58.7,38.3z' />
|
||||
<path d='M45.2,15.1c0.8-1.3,0.8-3.1,0-4.4l-5.1-8.1l-0.4-0.7C38.9,0.7,37.6,0,36.2,0H24.7c-1.4,0-2.7,0.7-3.5,1.9L0.6,34.9C0.2,35.5,0,36.3,0,37c0,0.8,0.2,1.5,0.6,2.2l5.5,8.8C6.9,49.3,8.2,50,9.7,50h11.5c1.4,0,2.8-0.7,3.5-1.9l0.4-0.7c0,0,0,0,0,0c0,0,0,0,0,0l4.1-6.6l12.1-19.4L45.2,15.1L45.2,15.1z M44,13c0,0.4-0.1,0.8-0.4,1.2L23.5,46.4c-0.2,0.3-0.5,0.3-0.6,0.3c-0.1,0-0.4,0-0.6-0.3l-5.1-8.2c-0.5-0.7-0.5-1.7,0-2.4L37.4,3.6c0.2-0.3,0.5-0.3,0.6-0.3c0.1,0,0.4,0,0.6,0.3l5.1,8.1C43.9,12.1,44,12.5,44,13z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function AsanaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'>
|
||||
@@ -4407,6 +4489,161 @@ export function DatadogIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function MicrosoftDataverseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const clip0 = `dataverse_clip0_${id}`
|
||||
const clip1 = `dataverse_clip1_${id}`
|
||||
const clip2 = `dataverse_clip2_${id}`
|
||||
const paint0 = `dataverse_paint0_${id}`
|
||||
const paint1 = `dataverse_paint1_${id}`
|
||||
const paint2 = `dataverse_paint2_${id}`
|
||||
const paint3 = `dataverse_paint3_${id}`
|
||||
const paint4 = `dataverse_paint4_${id}`
|
||||
const paint5 = `dataverse_paint5_${id}`
|
||||
const paint6 = `dataverse_paint6_${id}`
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width='96'
|
||||
height='96'
|
||||
viewBox='0 0 96 96'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<g clipPath={`url(#${clip0})`}>
|
||||
<g clipPath={`url(#${clip1})`}>
|
||||
<g clipPath={`url(#${clip2})`}>
|
||||
<path
|
||||
d='M13.8776 21.8242C29.1033 8.13791 49.7501 8.1861 62.955 18.9134C74.9816 28.6836 77.4697 44.3159 70.851 55.7801C64.2321 67.2443 52.5277 70.1455 39.5011 62.6247L31.7286 76.087L31.7234 76.0862C27.4181 83.5324 17.8937 86.0828 10.4437 81.7817C7.45394 80.0556 5.25322 77.4879 3.96665 74.551L3.96096 74.5511C-4.07832 55.7804 0.200745 34.1184 13.8776 21.8242Z'
|
||||
fill={`url(#${paint0})`}
|
||||
/>
|
||||
<path
|
||||
d='M13.8776 21.8242C29.1033 8.13791 49.7501 8.1861 62.955 18.9134C74.9816 28.6836 77.4697 44.3159 70.851 55.7801C64.2321 67.2443 52.5277 70.1455 39.5011 62.6247L31.7286 76.087L31.7234 76.0862C27.4181 83.5324 17.8937 86.0828 10.4437 81.7817C7.45394 80.0556 5.25322 77.4879 3.96665 74.551L3.96096 74.5511C-4.07832 55.7804 0.200745 34.1184 13.8776 21.8242Z'
|
||||
fill={`url(#${paint1})`}
|
||||
fillOpacity='0.8'
|
||||
/>
|
||||
<path
|
||||
d='M85.4327 14.2231C88.4528 15.9668 90.6686 18.569 91.9494 21.5433L91.9533 21.5444C99.9406 40.2943 95.6533 61.9068 81.9983 74.1814C66.7726 87.8677 46.1257 87.8196 32.9209 77.0923C20.8945 67.3221 18.4062 51.6897 25.0249 40.2256C31.6438 28.7614 43.3482 25.8601 56.3748 33.381L64.1434 19.9255L64.1482 19.9249C68.4516 12.4736 77.9805 9.92084 85.4327 14.2231Z'
|
||||
fill={`url(#${paint2})`}
|
||||
/>
|
||||
<path
|
||||
d='M85.4327 14.2231C88.4528 15.9668 90.6686 18.569 91.9494 21.5433L91.9533 21.5444C99.9406 40.2943 95.6533 61.9068 81.9983 74.1814C66.7726 87.8677 46.1257 87.8196 32.9209 77.0923C20.8945 67.3221 18.4062 51.6897 25.0249 40.2256C31.6438 28.7614 43.3482 25.8601 56.3748 33.381L64.1434 19.9255L64.1482 19.9249C68.4516 12.4736 77.9805 9.92084 85.4327 14.2231Z'
|
||||
fill={`url(#${paint3})`}
|
||||
fillOpacity='0.9'
|
||||
/>
|
||||
<path
|
||||
d='M39.5041 62.6261C52.5307 70.1469 64.2352 67.2456 70.8541 55.7814C77.2488 44.7055 75.1426 29.7389 64.147 19.9271L56.3791 33.3814L39.5041 62.6261Z'
|
||||
fill={`url(#${paint4})`}
|
||||
/>
|
||||
<path
|
||||
d='M56.3794 33.3815C43.3528 25.8607 31.6482 28.762 25.0294 40.2262C18.6347 51.3021 20.7409 66.2687 31.7364 76.0806L39.5043 62.6262L56.3794 33.3815Z'
|
||||
fill={`url(#${paint5})`}
|
||||
/>
|
||||
<path
|
||||
d='M33.3215 56.4453C37.9837 64.5204 48.3094 67.2872 56.3846 62.625C64.4598 57.9628 67.2266 47.6371 62.5643 39.5619C57.9021 31.4867 47.5764 28.72 39.5013 33.3822C31.4261 38.0444 28.6593 48.3701 33.3215 56.4453Z'
|
||||
fill={`url(#${paint6})`}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id={paint0}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(46.0001 49.4996) rotate(-148.717) scale(46.2195 47.5359)'
|
||||
>
|
||||
<stop offset='0.465088' stopColor='#09442A' />
|
||||
<stop offset='0.70088' stopColor='#136C6C' />
|
||||
<stop offset='1' stopColor='#22918B' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint1}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(50.0001 32.4996) rotate(123.57) scale(66.0095 46.5498)'
|
||||
>
|
||||
<stop offset='0.718705' stopColor='#1A7F7C' stopOpacity='0' />
|
||||
<stop offset='1' stopColor='#16BBDA' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint2}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(50.4999 44.5001) rotate(30.75) scale(45.9618 44.5095)'
|
||||
>
|
||||
<stop offset='0.358097' stopColor='#136C6C' />
|
||||
<stop offset='0.789474' stopColor='#42B870' />
|
||||
<stop offset='1' stopColor='#76D45E' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint3}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(42.5 -36.0002 31.1824 36.8127 49.4998 55.5001)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop offset='0.583166' stopColor='#76D45E' stopOpacity='0' />
|
||||
<stop offset='1' stopColor='#C8F5B7' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint4}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(47.5 48) rotate(-58.9042) scale(32.6898)'
|
||||
>
|
||||
<stop offset='0.486266' stopColor='#22918B' />
|
||||
<stop offset='0.729599' stopColor='#42B870' />
|
||||
<stop offset='1' stopColor='#43E5CA' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint5}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(47.3833 49.0077) rotate(119.859) scale(31.1328 29.4032)'
|
||||
>
|
||||
<stop offset='0.459553' stopColor='#08494E' />
|
||||
<stop offset='0.742242' stopColor='#1A7F7C' />
|
||||
<stop offset='1' stopColor='#309C61' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint6}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(52.5 40) rotate(120.784) scale(27.3542)'
|
||||
>
|
||||
<stop stopColor='#C8F5B7' />
|
||||
<stop offset='0.24583' stopColor='#98F0B0' />
|
||||
<stop offset='0.643961' stopColor='#52D17C' />
|
||||
<stop offset='1' stopColor='#119FC5' />
|
||||
</radialGradient>
|
||||
<clipPath id={clip0}>
|
||||
<rect width='96' height='96' fill='white' />
|
||||
</clipPath>
|
||||
<clipPath id={clip1}>
|
||||
<rect width='96' height='96' fill='white' />
|
||||
</clipPath>
|
||||
<clipPath id={clip2}>
|
||||
<rect width='95.9998' height='96' fill='white' />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function KalshiIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 78 20' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
|
||||
@@ -4809,6 +5046,26 @@ export function BedrockIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function TableIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
{...props}
|
||||
>
|
||||
<rect width='18' height='18' x='3' y='3' rx='2' />
|
||||
<path d='M3 9h18' />
|
||||
<path d='M3 15h18' />
|
||||
<path d='M9 3v18' />
|
||||
<path d='M15 3v18' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export function ReductoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -5239,6 +5496,34 @@ export function GoogleMapsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleTranslateIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 998.1 998.3'>
|
||||
<path
|
||||
fill='#DBDBDB'
|
||||
d='M931.7 998.3c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4H283.6l260.1 797.9h388z'
|
||||
/>
|
||||
<path
|
||||
fill='#DCDCDC'
|
||||
d='M931.7 230.4c9.7 0 18.9 3.8 25.8 10.6 6.8 6.7 10.6 15.5 10.6 24.8v667.1c0 9.3-3.7 18.1-10.6 24.8-6.9 6.8-16.1 10.6-25.8 10.6H565.5L324.9 230.4h606.8m0-30H283.6l260.1 797.9h388c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4z'
|
||||
/>
|
||||
<polygon fill='#4352B8' points='482.3,809.8 543.7,998.3 714.4,809.8' />
|
||||
<path
|
||||
fill='#607988'
|
||||
d='M936.1 476.1V437H747.6v-63.2h-61.2V437H566.1v39.1h239.4c-12.8 45.1-41.1 87.7-68.7 120.8-48.9-57.9-49.1-76.7-49.1-76.7h-50.8s2.1 28.2 70.7 108.6c-22.3 22.8-39.2 36.3-39.2 36.3l15.6 48.8s23.6-20.3 53.1-51.6c29.6 32.1 67.8 70.7 117.2 116.7l32.1-32.1c-52.9-48-91.7-86.1-120.2-116.7 38.2-45.2 77-102.1 85.2-154.2H936v.1z'
|
||||
/>
|
||||
<path
|
||||
fill='#4285F4'
|
||||
d='M66.4 0C29.9 0 0 29.9 0 66.5v677c0 36.5 29.9 66.4 66.4 66.4h648.1L454.4 0h-388z'
|
||||
/>
|
||||
<path
|
||||
fill='#EEEEEE'
|
||||
d='M371.4 430.6c-2.5 30.3-28.4 75.2-91.1 75.2-54.3 0-98.3-44.9-98.3-100.2s44-100.2 98.3-100.2c30.9 0 51.5 13.4 63.3 24.3l41.2-39.6c-27.1-25-62.4-40.6-104.5-40.6-86.1 0-156 69.9-156 156s69.9 156 156 156c90.2 0 149.8-63.3 149.8-152.6 0-12.8-1.6-22.2-3.7-31.8h-146v53.4l91 .1z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DsPyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='30 28 185 175' fill='none'>
|
||||
@@ -5532,3 +5817,116 @@ export function OnePasswordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function VercelIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 256 222'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
preserveAspectRatio='xMidYMid'
|
||||
>
|
||||
<g transform='translate(19.2 16.63) scale(0.85)'>
|
||||
<polygon fill='#fafafa' points='128 0 256 221.705007 0 221.705007' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CloudflareIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'>
|
||||
<path
|
||||
fill='#f38020'
|
||||
d='M331 326c11-26-4-38-19-38l-148-2c-4 0-4-6 1-7l150-2c17-1 37-15 43-33 0 0 10-21 9-24a97 97 0 0 0-187-11c-38-25-78 9-69 46-48 3-65 46-60 72 0 1 1 2 3 2h274c1 0 3-1 3-3z'
|
||||
/>
|
||||
<path
|
||||
fill='#faae40'
|
||||
d='M381 224c-4 0-6-1-7 1l-5 21c-5 16 3 30 20 31l32 2c4 0 4 6-1 7l-33 1c-36 4-46 39-46 39 0 2 0 3 2 3h113l3-2a81 81 0 0 0-78-103'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function UpstashIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 341' width='24' height='24'>
|
||||
<path
|
||||
fill='#00C98D'
|
||||
d='M0 298.417c56.554 56.553 148.247 56.553 204.801 0c56.554-56.554 56.554-148.247 0-204.801l-25.6 25.6c42.415 42.416 42.415 111.185 0 153.6c-42.416 42.416-111.185 42.416-153.601 0z'
|
||||
/>
|
||||
<path
|
||||
fill='#00C98D'
|
||||
d='M51.2 247.216c28.277 28.277 74.123 28.277 102.4 0c28.277-28.276 28.277-74.123 0-102.4l-25.6 25.6c14.14 14.138 14.14 37.061 0 51.2c-14.138 14.139-37.061 14.139-51.2 0zM256 42.415c-56.554-56.553-148.247-56.553-204.8 0c-56.555 56.555-56.555 148.247 0 204.801l25.599-25.6c-42.415-42.415-42.415-111.185 0-153.6c42.416-42.416 111.185-42.416 153.6 0z'
|
||||
/>
|
||||
<path
|
||||
fill='#00C98D'
|
||||
d='M204.8 93.616c-28.276-28.277-74.124-28.277-102.4 0c-28.278 28.277-28.278 74.123 0 102.4l25.6-25.6c-14.14-14.138-14.14-37.061 0-51.2c14.138-14.139 37.06-14.139 51.2 0z'
|
||||
/>
|
||||
<path
|
||||
fill='#FFF'
|
||||
fillOpacity='.4'
|
||||
d='M256 42.415c-56.554-56.553-148.247-56.553-204.8 0c-56.555 56.555-56.555 148.247 0 204.801l25.599-25.6c-42.415-42.415-42.415-111.185 0-153.6c42.416-42.416 111.185-42.416 153.6 0z'
|
||||
/>
|
||||
<path
|
||||
fill='#FFF'
|
||||
fillOpacity='.4'
|
||||
d='M204.8 93.616c-28.276-28.277-74.124-28.277-102.4 0c-28.278 28.277-28.278 74.123 0 102.4l25.6-25.6c-14.14-14.138-14.14-37.061 0-51.2c14.138-14.139 37.06-14.139 51.2 0z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function RevenueCatIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width='512'
|
||||
height='512'
|
||||
viewBox='0 0 512 512'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M95 109.774C110.152 106.108 133.612 104 154.795 104C212.046 104 246.32 123.928 246.32 174.646C246.32 205.746 233.737 226.264 214.005 237.437L261.765 318.946C258.05 321.632 250.035 323.176 238.864 323.176C226.282 323.176 217.987 321.672 211.982 318.946L172.225 248.3H167.645C157.789 248.305 147.945 247.601 138.18 246.192V319.255C134.172 321.672 127.022 323.176 116.73 323.176C106.73 323.176 99.2874 321.659 95 319.255V109.774ZM137.643 207.848C145.772 209.263 153.997 209.968 162.235 209.956C187.12 209.956 202.285 200.556 202.285 177.057C202.285 152.886 186.268 142.949 157.668 142.949C150.956 142.918 144.255 143.515 137.643 144.735V207.848Z'
|
||||
fill='#FFFFFF'
|
||||
/>
|
||||
<path
|
||||
d='M428.529 329.244C428.529 365.526 410.145 375.494 396.306 382.195C360.972 399.32 304.368 379.4 244.206 373.338C189.732 366.214 135.706 361.522 127.309 373.738C124.152 376.832 123.481 386.798 127.309 390.862C138.604 402.85 168.061 394.493 188.919 390.714C195.391 389.694 201.933 392.099 206.079 397.021C210.226 401.944 211.349 408.637 209.024 414.58C206.699 420.522 201.28 424.811 194.809 425.831C185.379 427.264 175.85 427.989 166.306 428C145.988 428 120.442 424.495 105.943 409.072C98.7232 401.4 91.3266 387.78 97.0271 366.465C107.875 326.074 172.807 336.052 248.033 343.633C300.41 348.907 357.23 366.465 379.934 350.343C385.721 346.234 396.517 337.022 390.698 329.244C384.879 321.467 375.353 325.684 362.838 325.684C300.152 325.684 263.238 285.302 263.238 217.916C263.247 167.292 284.176 131.892 318.287 115.09C333.109 107.789 350.421 104 369.587 104C386.292 104 403.269 106.931 414.11 113.366C420.847 123.032 423.778 140.305 422.306 153.201C408.247 146.466 395.36 142.949 378.669 142.949C337.365 142.949 308.947 164.039 308.947 214.985C308.947 265.932 337.065 286.149 376.611 286.149C387.869 286.035 403.1 284.67 422.306 282.053C426.455 297.498 428.529 313.228 428.529 329.244Z'
|
||||
fill='#FFFFFF'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function RedisIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 512 512'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
strokeLinejoin='round'
|
||||
strokeMiterlimit='2'
|
||||
>
|
||||
<path
|
||||
d='M479.14 279.864c-34.584 43.578-71.94 93.385-146.645 93.385-66.73 0-91.59-58.858-93.337-106.672 14.62 30.915 43.203 55.949 87.804 54.792C412.737 318.6 471.53 241.127 471.53 170.57c0-84.388-62.947-145.262-172.24-145.262-78.165 0-175.004 29.743-238.646 76.782-.689 48.42 26.286 111.369 35.972 104.452 55.17-39.67 98.918-65.203 141.35-78.01C175.153 198.58 24.451 361.219 6 389.85c2.076 26.286 34.588 96.842 50.496 96.842 4.841 0 8.993-2.768 13.835-7.61 45.433-51.046 82.472-96.816 115.412-140.933 4.627 64.658 36.42 143.702 125.307 143.702 79.55 0 158.408-57.414 194.377-186.767 4.149-15.911-15.22-28.362-26.286-15.22zm-90.616-104.449c0 40.81-40.118 60.87-76.782 60.87-19.596 0-34.648-5.145-46.554-11.832 21.906-33.168 43.59-67.182 66.887-103.593 41.08 6.953 56.449 29.788 56.449 54.555z'
|
||||
fill='#FFFFFF'
|
||||
fillRule='nonzero'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function HexIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1450.3 600'>
|
||||
<path
|
||||
fill='#EDB9B8'
|
||||
fillRule='evenodd'
|
||||
d='m250.11,0v199.49h-50V0H0v600h200.11v-300.69h50v300.69h200.18V0h-200.18Zm249.9,0v600h450.29v-250.23h-200.2v149h-50v-199.46h250.2V0h-450.29Zm200.09,199.49v-99.49h50v99.49h-50Zm550.02,0V0h200.18v150l-100,100.09,100,100.09v249.82h-200.18v-300.69h-50v300.69h-200.11v-249.82l100.11-100.09-100.11-100.09V0h200.11v199.49h50Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
AhrefsIcon,
|
||||
AirtableIcon,
|
||||
AirweaveIcon,
|
||||
AlgoliaIcon,
|
||||
ApifyIcon,
|
||||
ApolloIcon,
|
||||
ArxivIcon,
|
||||
AsanaIcon,
|
||||
AttioIcon,
|
||||
BrainIcon,
|
||||
BrowserUseIcon,
|
||||
CalComIcon,
|
||||
@@ -19,9 +21,11 @@ import {
|
||||
CirclebackIcon,
|
||||
ClayIcon,
|
||||
ClerkIcon,
|
||||
CloudflareIcon,
|
||||
ConfluenceIcon,
|
||||
CursorIcon,
|
||||
DatadogIcon,
|
||||
DevinIcon,
|
||||
DiscordIcon,
|
||||
DocumentIcon,
|
||||
DropboxIcon,
|
||||
@@ -38,6 +42,8 @@ import {
|
||||
GithubIcon,
|
||||
GitLabIcon,
|
||||
GmailIcon,
|
||||
GongIcon,
|
||||
GoogleBigQueryIcon,
|
||||
GoogleBooksIcon,
|
||||
GoogleCalendarIcon,
|
||||
GoogleDocsIcon,
|
||||
@@ -48,10 +54,13 @@ import {
|
||||
GoogleMapsIcon,
|
||||
GoogleSheetsIcon,
|
||||
GoogleSlidesIcon,
|
||||
GoogleTasksIcon,
|
||||
GoogleTranslateIcon,
|
||||
GoogleVaultIcon,
|
||||
GrafanaIcon,
|
||||
GrainIcon,
|
||||
GreptileIcon,
|
||||
HexIcon,
|
||||
HubspotIcon,
|
||||
HuggingFaceIcon,
|
||||
HunterIOIcon,
|
||||
@@ -71,6 +80,7 @@ import {
|
||||
MailgunIcon,
|
||||
MailServerIcon,
|
||||
Mem0Icon,
|
||||
MicrosoftDataverseIcon,
|
||||
MicrosoftExcelIcon,
|
||||
MicrosoftOneDriveIcon,
|
||||
MicrosoftPlannerIcon,
|
||||
@@ -96,8 +106,10 @@ import {
|
||||
QdrantIcon,
|
||||
RDSIcon,
|
||||
RedditIcon,
|
||||
RedisIcon,
|
||||
ReductoIcon,
|
||||
ResendIcon,
|
||||
RevenueCatIcon,
|
||||
S3Icon,
|
||||
SalesforceIcon,
|
||||
SearchIcon,
|
||||
@@ -125,6 +137,8 @@ import {
|
||||
TTSIcon,
|
||||
TwilioIcon,
|
||||
TypeformIcon,
|
||||
UpstashIcon,
|
||||
VercelIcon,
|
||||
VideoIcon,
|
||||
WealthboxIcon,
|
||||
WebflowIcon,
|
||||
@@ -145,19 +159,23 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
ahrefs: AhrefsIcon,
|
||||
airtable: AirtableIcon,
|
||||
airweave: AirweaveIcon,
|
||||
algolia: AlgoliaIcon,
|
||||
apify: ApifyIcon,
|
||||
apollo: ApolloIcon,
|
||||
arxiv: ArxivIcon,
|
||||
asana: AsanaIcon,
|
||||
attio: AttioIcon,
|
||||
browser_use: BrowserUseIcon,
|
||||
calcom: CalComIcon,
|
||||
calendly: CalendlyIcon,
|
||||
circleback: CirclebackIcon,
|
||||
clay: ClayIcon,
|
||||
clerk: ClerkIcon,
|
||||
cloudflare: CloudflareIcon,
|
||||
confluence_v2: ConfluenceIcon,
|
||||
cursor_v2: CursorIcon,
|
||||
datadog: DatadogIcon,
|
||||
devin: DevinIcon,
|
||||
discord: DiscordIcon,
|
||||
dropbox: DropboxIcon,
|
||||
dspy: DsPyIcon,
|
||||
@@ -173,6 +191,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
github_v2: GithubIcon,
|
||||
gitlab: GitLabIcon,
|
||||
gmail_v2: GmailIcon,
|
||||
gong: GongIcon,
|
||||
google_bigquery: GoogleBigQueryIcon,
|
||||
google_books: GoogleBooksIcon,
|
||||
google_calendar_v2: GoogleCalendarIcon,
|
||||
google_docs: GoogleDocsIcon,
|
||||
@@ -183,10 +203,13 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
google_search: GoogleIcon,
|
||||
google_sheets_v2: GoogleSheetsIcon,
|
||||
google_slides_v2: GoogleSlidesIcon,
|
||||
google_tasks: GoogleTasksIcon,
|
||||
google_translate: GoogleTranslateIcon,
|
||||
google_vault: GoogleVaultIcon,
|
||||
grafana: GrafanaIcon,
|
||||
grain: GrainIcon,
|
||||
greptile: GreptileIcon,
|
||||
hex: HexIcon,
|
||||
hubspot: HubspotIcon,
|
||||
huggingface: HuggingFaceIcon,
|
||||
hunter: HunterIOIcon,
|
||||
@@ -208,6 +231,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
mailgun: MailgunIcon,
|
||||
mem0: Mem0Icon,
|
||||
memory: BrainIcon,
|
||||
microsoft_dataverse: MicrosoftDataverseIcon,
|
||||
microsoft_excel_v2: MicrosoftExcelIcon,
|
||||
microsoft_planner: MicrosoftPlannerIcon,
|
||||
microsoft_teams: MicrosoftTeamsIcon,
|
||||
@@ -231,8 +255,10 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
qdrant: QdrantIcon,
|
||||
rds: RDSIcon,
|
||||
reddit: RedditIcon,
|
||||
redis: RedisIcon,
|
||||
reducto_v2: ReductoIcon,
|
||||
resend: ResendIcon,
|
||||
revenuecat: RevenueCatIcon,
|
||||
s3: S3Icon,
|
||||
salesforce: SalesforceIcon,
|
||||
search: SearchIcon,
|
||||
@@ -262,6 +288,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
twilio_sms: TwilioIcon,
|
||||
twilio_voice: TwilioIcon,
|
||||
typeform: TypeformIcon,
|
||||
upstash: UpstashIcon,
|
||||
vercel: VercelIcon,
|
||||
video_generator_v2: VideoIcon,
|
||||
vision_v2: EyeIcon,
|
||||
wealthbox: WealthboxIcon,
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: Umgebungsvariablen
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Umgebungsvariablen bieten eine sichere Möglichkeit, Konfigurationswerte und Geheimnisse in Ihren Workflows zu verwalten, einschließlich API-Schlüssel und anderer sensibler Daten, auf die Ihre Workflows zugreifen müssen. Sie halten Geheimnisse aus Ihren Workflow-Definitionen heraus und machen sie während der Ausführung verfügbar.
|
||||
|
||||
## Variablentypen
|
||||
|
||||
Umgebungsvariablen in Sim funktionieren auf zwei Ebenen:
|
||||
|
||||
- **Persönliche Umgebungsvariablen**: Privat für Ihr Konto, nur Sie können sie sehen und verwenden
|
||||
- **Workspace-Umgebungsvariablen**: Werden im gesamten Workspace geteilt und sind für alle Teammitglieder verfügbar
|
||||
|
||||
<Callout type="info">
|
||||
Workspace-Umgebungsvariablen haben Vorrang vor persönlichen Variablen, wenn es einen Namenskonflikt gibt.
|
||||
</Callout>
|
||||
|
||||
## Einrichten von Umgebungsvariablen
|
||||
|
||||
Navigieren Sie zu den Einstellungen, um Ihre Umgebungsvariablen zu konfigurieren:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="Umgebungsvariablen-Modal zum Erstellen neuer Variablen"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
In Ihren Workspace-Einstellungen können Sie sowohl persönliche als auch Workspace-Umgebungsvariablen erstellen und verwalten. Persönliche Variablen sind privat für Ihr Konto, während Workspace-Variablen mit allen Teammitgliedern geteilt werden.
|
||||
|
||||
### Variablen auf Workspace-Ebene setzen
|
||||
|
||||
Verwenden Sie den Workspace-Bereichsschalter, um Variablen für Ihr gesamtes Team verfügbar zu machen:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="Workspace-Bereich für Umgebungsvariablen umschalten"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
Wenn Sie den Workspace-Bereich aktivieren, wird die Variable für alle Workspace-Mitglieder verfügbar und kann in jedem Workflow innerhalb dieses Workspaces verwendet werden.
|
||||
|
||||
### Ansicht der Workspace-Variablen
|
||||
|
||||
Sobald Sie Workspace-Variablen haben, erscheinen sie in Ihrer Liste der Umgebungsvariablen:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="Workspace-Variablen in der Liste der Umgebungsvariablen"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Verwendung von Variablen in Workflows
|
||||
|
||||
Um Umgebungsvariablen in Ihren Workflows zu referenzieren, verwenden Sie die `{{}}` Notation. Wenn Sie `{{` in ein beliebiges Eingabefeld eingeben, erscheint ein Dropdown-Menü mit Ihren persönlichen und Workspace-Umgebungsvariablen. Wählen Sie einfach die Variable aus, die Sie verwenden möchten.
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="Verwendung von Umgebungsvariablen mit doppelter Klammernotation"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Wie Variablen aufgelöst werden
|
||||
|
||||
**Workspace-Variablen haben immer Vorrang** vor persönlichen Variablen, unabhängig davon, wer den Workflow ausführt.
|
||||
|
||||
Wenn keine Workspace-Variable für einen Schlüssel existiert, werden persönliche Variablen verwendet:
|
||||
- **Manuelle Ausführungen (UI)**: Ihre persönlichen Variablen
|
||||
- **Automatisierte Ausführungen (API, Webhook, Zeitplan, bereitgestellter Chat)**: Persönliche Variablen des Workflow-Besitzers
|
||||
|
||||
<Callout type="info">
|
||||
Persönliche Variablen eignen sich am besten zum Testen. Verwenden Sie Workspace-Variablen für Produktions-Workflows.
|
||||
</Callout>
|
||||
|
||||
## Sicherheits-Best-Practices
|
||||
|
||||
### Für sensible Daten
|
||||
- Speichern Sie API-Schlüssel, Tokens und Passwörter als Umgebungsvariablen anstatt sie im Code festzuschreiben
|
||||
- Verwenden Sie Workspace-Variablen für gemeinsam genutzte Ressourcen, die mehrere Teammitglieder benötigen
|
||||
- Bewahren Sie persönliche Anmeldedaten in persönlichen Variablen auf
|
||||
|
||||
### Variablenbenennung
|
||||
- Verwenden Sie beschreibende Namen: `DATABASE_URL` anstatt `DB`
|
||||
- Folgen Sie einheitlichen Benennungskonventionen in Ihrem Team
|
||||
- Erwägen Sie Präfixe, um Konflikte zu vermeiden: `PROD_API_KEY`, `DEV_API_KEY`
|
||||
|
||||
### Zugriffskontrolle
|
||||
- Workspace-Umgebungsvariablen respektieren Workspace-Berechtigungen
|
||||
- Nur Benutzer mit Schreibzugriff oder höher können Workspace-Variablen erstellen/ändern
|
||||
- Persönliche Variablen sind immer privat für den einzelnen Benutzer
|
||||
@@ -95,11 +95,17 @@ const apiUrl = `https://api.example.com/users/${userId}/profile`;
|
||||
|
||||
### Request Retries
|
||||
|
||||
The API block automatically handles:
|
||||
- Network timeouts with exponential backoff
|
||||
- Rate limit responses (429 status codes)
|
||||
- Server errors (5xx status codes) with retry logic
|
||||
- Connection failures with reconnection attempts
|
||||
The API block supports **configurable retries** (see the block’s **Advanced** settings):
|
||||
|
||||
- **Retries**: Number of retry attempts (additional tries after the first request)
|
||||
- **Retry delay (ms)**: Initial delay before retrying (uses exponential backoff)
|
||||
- **Max retry delay (ms)**: Maximum delay between retries
|
||||
- **Retry non-idempotent methods**: Allow retries for **POST/PATCH** (may create duplicate requests)
|
||||
|
||||
Retries are attempted for:
|
||||
|
||||
- Network/connection failures and timeouts (with exponential backoff)
|
||||
- Rate limits (**429**) and server errors (**5xx**)
|
||||
|
||||
### Response Validation
|
||||
|
||||
|
||||
192
apps/docs/content/docs/en/credentials/index.mdx
Normal file
192
apps/docs/content/docs/en/credentials/index.mdx
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: Credentials
|
||||
description: Manage secrets, API keys, and OAuth connections for your workflows
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
|
||||
Credentials provide a secure way to manage API keys, tokens, and third-party service connections across your workflows. Instead of hardcoding sensitive values into your workflow, you store them as credentials and reference them at runtime.
|
||||
|
||||
Sim supports two categories of credentials: **secrets** for static values like API keys, and **OAuth accounts** for authenticated service connections like Google or Slack.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To manage credentials, open your workspace **Settings** and navigate to the **Secrets** tab.
|
||||
|
||||
<Image
|
||||
src="/static/credentials/settings-secrets.png"
|
||||
alt="Settings modal showing the Secrets tab with a list of saved credentials"
|
||||
width={700}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
From here you can search, create, and delete both secrets and OAuth connections.
|
||||
|
||||
## Secrets
|
||||
|
||||
Secrets are key-value pairs that store sensitive data like API keys, tokens, and passwords. Each secret has a **key** (used to reference it in workflows) and a **value** (the actual secret).
|
||||
|
||||
### Creating a Secret
|
||||
|
||||
<Image
|
||||
src="/static/credentials/create-secret.png"
|
||||
alt="Create Secret dialog with fields for key, value, description, and scope toggle"
|
||||
width={500}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Click **+ Add** and select **Secret** as the type
|
||||
</Step>
|
||||
<Step>
|
||||
Enter a **Key** name (letters, numbers, and underscores only, e.g. `OPENAI_API_KEY`)
|
||||
</Step>
|
||||
<Step>
|
||||
Enter the **Value**
|
||||
</Step>
|
||||
<Step>
|
||||
Optionally add a **Description** to help your team understand what the secret is for
|
||||
</Step>
|
||||
<Step>
|
||||
Choose the **Scope** — Workspace or Personal
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Create**
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Using Secrets in Workflows
|
||||
|
||||
To reference a secret in any input field, type `{{` to open the dropdown. It will show your available secrets grouped by scope.
|
||||
|
||||
<Image
|
||||
src="/static/credentials/secret-dropdown.png"
|
||||
alt="Typing {{ in a code block opens a dropdown showing available workspace secrets"
|
||||
width={400}
|
||||
height={250}
|
||||
/>
|
||||
|
||||
Select the secret you want to use. The reference will appear highlighted in blue, indicating it will be resolved at runtime.
|
||||
|
||||
<Image
|
||||
src="/static/credentials/secret-resolved.png"
|
||||
alt="A resolved secret reference shown in blue text as {{OPENAI_API_KEY}}"
|
||||
width={400}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
<Callout type="warn">
|
||||
Secret values are never exposed in the workflow editor or logs. They are only resolved during execution.
|
||||
</Callout>
|
||||
|
||||
### Bulk Import
|
||||
|
||||
You can import multiple secrets at once by pasting `.env`-style content:
|
||||
|
||||
1. Click **+ Add**, then switch to **Bulk** mode
|
||||
2. Paste your environment variables in `KEY=VALUE` format
|
||||
3. Choose the scope for all imported secrets
|
||||
4. Click **Create**
|
||||
|
||||
The parser supports standard `KEY=VALUE` pairs, quoted values, comments (`#`), and blank lines.
|
||||
|
||||
## OAuth Accounts
|
||||
|
||||
OAuth accounts are authenticated connections to third-party services like Google, Slack, GitHub, and more. Sim handles the OAuth flow, token storage, and automatic refresh.
|
||||
|
||||
You can connect **multiple accounts per provider** — for example, two separate Gmail accounts for different workflows.
|
||||
|
||||
### Connecting an OAuth Account
|
||||
|
||||
<Image
|
||||
src="/static/credentials/create-oauth.png"
|
||||
alt="Create Secret dialog with OAuth Account type selected, showing display name and provider dropdown"
|
||||
width={500}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Click **+ Add** and select **OAuth Account** as the type
|
||||
</Step>
|
||||
<Step>
|
||||
Enter a **Display name** to identify this connection (e.g. "Work Gmail" or "Marketing Slack")
|
||||
</Step>
|
||||
<Step>
|
||||
Optionally add a **Description**
|
||||
</Step>
|
||||
<Step>
|
||||
Select the **Account** provider from the dropdown
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Connect** and complete the authorization flow
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Using OAuth Accounts in Workflows
|
||||
|
||||
Blocks that require authentication (e.g. Gmail, Slack, Google Sheets) display a credential selector dropdown. Select the OAuth account you want the block to use.
|
||||
|
||||
<Image
|
||||
src="/static/credentials/oauth-selector.png"
|
||||
alt="Gmail block showing the account selector dropdown with a connected account and option to connect another"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
You can also connect additional accounts directly from the block by selecting **Connect another account** at the bottom of the dropdown.
|
||||
|
||||
<Callout type="info">
|
||||
If a block requires an OAuth connection and none is selected, the workflow will fail at that step.
|
||||
</Callout>
|
||||
|
||||
## Workspace vs. Personal
|
||||
|
||||
Credentials can be scoped to your **workspace** (shared with your team) or kept **personal** (private to you).
|
||||
|
||||
| | Workspace | Personal |
|
||||
|---|---|---|
|
||||
| **Visibility** | All workspace members | Only you |
|
||||
| **Use in workflows** | Any member can use | Only you can use |
|
||||
| **Best for** | Production workflows, shared services | Testing, personal API keys |
|
||||
| **Who can edit** | Workspace admins | Only you |
|
||||
| **Auto-shared** | Yes — all members get access on creation | No — only you have access |
|
||||
|
||||
<Callout type="info">
|
||||
When a workspace and personal secret share the same key name, the **workspace secret takes precedence**.
|
||||
</Callout>
|
||||
|
||||
### Resolution Order
|
||||
|
||||
When a workflow runs, Sim resolves secrets in this order:
|
||||
|
||||
1. **Workspace secrets** are checked first
|
||||
2. **Personal secrets** are used as a fallback — from the user who triggered the run (manual) or the workflow owner (automated runs via API, webhook, or schedule)
|
||||
|
||||
## Access Control
|
||||
|
||||
Each credential has role-based access control:
|
||||
|
||||
- **Admin** — can view, edit, delete, and manage who has access
|
||||
- **Member** — can use the credential in workflows (read-only)
|
||||
|
||||
When you create a workspace secret, all current workspace members are automatically granted access. Personal secrets are only accessible to you by default.
|
||||
|
||||
### Sharing a Credential
|
||||
|
||||
To share a credential with specific team members:
|
||||
|
||||
1. Click **Details** on the credential
|
||||
2. Invite members by email
|
||||
3. Assign them an **Admin** or **Member** role
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use workspace credentials for production** so workflows work regardless of who triggers them
|
||||
- **Use personal credentials for development** to keep your test keys separate
|
||||
- **Name keys descriptively** — `STRIPE_SECRET_KEY` over `KEY1`
|
||||
- **Connect multiple OAuth accounts** when you need different permissions or identities per workflow
|
||||
- **Never hardcode secrets** in workflow input fields — always use `{{KEY}}` references
|
||||
@@ -97,6 +97,7 @@ Understanding these core principles will help you build better workflows:
|
||||
3. **Smart Data Flow**: Outputs flow automatically to connected blocks
|
||||
4. **Error Handling**: Failed blocks stop their execution path but don't affect independent paths
|
||||
5. **State Persistence**: All block outputs and execution details are preserved for debugging
|
||||
6. **Cycle Protection**: Workflows that call other workflows (via Workflow blocks, MCP tools, or API blocks) are tracked with a call chain. If the chain exceeds 25 hops, execution is stopped to prevent infinite loops
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"skills",
|
||||
"knowledgebase",
|
||||
"variables",
|
||||
"credentials",
|
||||
"execution",
|
||||
"permissions",
|
||||
"sdks",
|
||||
|
||||
404
apps/docs/content/docs/en/tools/algolia.mdx
Normal file
404
apps/docs/content/docs/en/tools/algolia.mdx
Normal file
@@ -0,0 +1,404 @@
|
||||
---
|
||||
title: Algolia
|
||||
description: Search and manage Algolia indices
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="algolia"
|
||||
color="#003DFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Algolia](https://www.algolia.com/) is a powerful hosted search platform that enables developers and teams to deliver fast, relevant search experiences in their apps and websites. Algolia provides full-text, faceted, and filtered search as well as analytics and advanced ranking capabilities.
|
||||
|
||||
With Algolia, you can:
|
||||
|
||||
- **Deliver lightning-fast search**: Provide instant search results as users type, with typo tolerance and synonyms
|
||||
- **Manage and update records**: Easily add, update, or delete objects/records in your indices
|
||||
- **Perform advanced filtering**: Use filters, facets, and custom ranking to refine and organize search results
|
||||
- **Configure index settings**: Adjust relevance, ranking, attributes for search, and more to optimize user experience
|
||||
- **Scale confidently**: Algolia handles massive traffic and data volumes with globally distributed infrastructure
|
||||
- **Gain insights**: Track analytics, search patterns, and user engagement
|
||||
|
||||
In Sim, the Algolia integration allows your agents to search, manage, and configure Algolia indices directly within your workflows. Use Algolia to power dynamic data exploration, automate record updates, run batch operations, and more—all from a single tool in your workspace.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Algolia into your workflow. Search indices, manage records (add, update, delete, browse), configure index settings, and perform batch operations.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `algolia_search`
|
||||
|
||||
Search an Algolia index
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia API Key |
|
||||
| `indexName` | string | Yes | Name of the Algolia index to search |
|
||||
| `query` | string | Yes | Search query text |
|
||||
| `hitsPerPage` | number | No | Number of hits per page \(default: 20\) |
|
||||
| `page` | number | No | Page number to retrieve \(default: 0\) |
|
||||
| `filters` | string | No | Filter string \(e.g., "category:electronics AND price < 100"\) |
|
||||
| `attributesToRetrieve` | string | No | Comma-separated list of attributes to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `hits` | array | Array of matching records |
|
||||
| ↳ `objectID` | string | Unique identifier of the record |
|
||||
| ↳ `_highlightResult` | object | Highlighted attributes matching the query. Each attribute has value, matchLevel \(none, partial, full\), and matchedWords |
|
||||
| ↳ `_snippetResult` | object | Snippeted attributes matching the query. Each attribute has value and matchLevel |
|
||||
| ↳ `_rankingInfo` | object | Ranking information for the hit. Only present when getRankingInfo is enabled |
|
||||
| ↳ `nbTypos` | number | Number of typos in the query match |
|
||||
| ↳ `firstMatchedWord` | number | Position of the first matched word |
|
||||
| ↳ `geoDistance` | number | Distance in meters for geo-search results |
|
||||
| ↳ `nbExactWords` | number | Number of exactly matched words |
|
||||
| ↳ `userScore` | number | Custom ranking score |
|
||||
| ↳ `words` | number | Number of matched words |
|
||||
| `nbHits` | number | Total number of matching hits |
|
||||
| `page` | number | Current page number \(zero-based\) |
|
||||
| `nbPages` | number | Total number of pages available |
|
||||
| `hitsPerPage` | number | Number of hits per page \(1-1000, default 20\) |
|
||||
| `processingTimeMS` | number | Server-side processing time in milliseconds |
|
||||
| `query` | string | The search query that was executed |
|
||||
| `parsedQuery` | string | The query string after normalization and stop word removal |
|
||||
| `facets` | object | Facet counts keyed by facet name, each containing value-count pairs |
|
||||
| `facets_stats` | object | Statistics \(min, max, avg, sum\) for numeric facets |
|
||||
| `exhaustive` | object | Exhaustiveness flags for facetsCount, facetValues, nbHits, rulesMatch, and typo |
|
||||
|
||||
### `algolia_add_record`
|
||||
|
||||
Add or replace a record in an Algolia index
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia Admin API Key |
|
||||
| `indexName` | string | Yes | Name of the Algolia index |
|
||||
| `objectID` | string | No | Object ID for the record \(auto-generated if not provided\) |
|
||||
| `record` | json | Yes | JSON object representing the record to add |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskID` | number | Algolia task ID for tracking the indexing operation |
|
||||
| `objectID` | string | The object ID of the added or replaced record |
|
||||
| `createdAt` | string | Timestamp when the record was created \(only present when objectID is auto-generated\) |
|
||||
| `updatedAt` | string | Timestamp when the record was updated \(only present when replacing an existing record\) |
|
||||
|
||||
### `algolia_get_record`
|
||||
|
||||
Get a record by objectID from an Algolia index
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia API Key |
|
||||
| `indexName` | string | Yes | Name of the Algolia index |
|
||||
| `objectID` | string | Yes | The objectID of the record to retrieve |
|
||||
| `attributesToRetrieve` | string | No | Comma-separated list of attributes to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `objectID` | string | The objectID of the retrieved record |
|
||||
| `record` | object | The record data \(all attributes\) |
|
||||
|
||||
### `algolia_get_records`
|
||||
|
||||
Retrieve multiple records by objectID from one or more Algolia indices
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia API Key |
|
||||
| `indexName` | string | Yes | Default index name for all requests |
|
||||
| `requests` | json | Yes | Array of objects specifying records to retrieve. Each must have "objectID" and optionally "indexName" and "attributesToRetrieve". |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | array | Array of retrieved records \(null entries for records not found\) |
|
||||
| ↳ `objectID` | string | Unique identifier of the record |
|
||||
|
||||
### `algolia_partial_update_record`
|
||||
|
||||
Partially update a record in an Algolia index without replacing it entirely
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia Admin API Key |
|
||||
| `indexName` | string | Yes | Name of the Algolia index |
|
||||
| `objectID` | string | Yes | The objectID of the record to update |
|
||||
| `attributes` | json | Yes | JSON object with attributes to update. Supports built-in operations like \{"stock": \{"_operation": "Decrement", "value": 1\}\} |
|
||||
| `createIfNotExists` | boolean | No | Whether to create the record if it does not exist \(default: true\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskID` | number | Algolia task ID for tracking the update operation |
|
||||
| `objectID` | string | The objectID of the updated record |
|
||||
| `updatedAt` | string | Timestamp when the record was updated |
|
||||
|
||||
### `algolia_delete_record`
|
||||
|
||||
Delete a record by objectID from an Algolia index
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia Admin API Key |
|
||||
| `indexName` | string | Yes | Name of the Algolia index |
|
||||
| `objectID` | string | Yes | The objectID of the record to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskID` | number | Algolia task ID for tracking the deletion |
|
||||
| `deletedAt` | string | Timestamp when the record was deleted |
|
||||
|
||||
### `algolia_browse_records`
|
||||
|
||||
Browse and iterate over all records in an Algolia index using cursor pagination
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia API Key \(must have browse ACL\) |
|
||||
| `indexName` | string | Yes | Name of the Algolia index to browse |
|
||||
| `query` | string | No | Search query to filter browsed records |
|
||||
| `filters` | string | No | Filter string to narrow down results |
|
||||
| `attributesToRetrieve` | string | No | Comma-separated list of attributes to retrieve |
|
||||
| `hitsPerPage` | number | No | Number of hits per page \(default: 1000, max: 1000\) |
|
||||
| `cursor` | string | No | Cursor from a previous browse response for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `hits` | array | Array of records from the index \(up to 1000 per request\) |
|
||||
| ↳ `objectID` | string | Unique identifier of the record |
|
||||
| `cursor` | string | Opaque cursor string for retrieving the next page of results. Absent when no more results exist. |
|
||||
| `nbHits` | number | Total number of records matching the browse criteria |
|
||||
| `page` | number | Current page number \(zero-based\) |
|
||||
| `nbPages` | number | Total number of pages available |
|
||||
| `hitsPerPage` | number | Number of hits per page \(1-1000, default 1000 for browse\) |
|
||||
| `processingTimeMS` | number | Server-side processing time in milliseconds |
|
||||
|
||||
### `algolia_batch_operations`
|
||||
|
||||
Perform batch add, update, partial update, or delete operations on records in an Algolia index
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia Admin API Key |
|
||||
| `indexName` | string | Yes | Name of the Algolia index |
|
||||
| `requests` | json | Yes | Array of batch operations. Each item has "action" \(addObject, updateObject, partialUpdateObject, partialUpdateObjectNoCreate, deleteObject\) and "body" \(the record data, must include objectID for update/delete\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskID` | number | Algolia task ID for tracking the batch operation |
|
||||
| `objectIDs` | array | Array of object IDs affected by the batch operation |
|
||||
|
||||
### `algolia_list_indices`
|
||||
|
||||
List all indices in an Algolia application
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia API Key |
|
||||
| `page` | number | No | Page number for paginating indices \(default: not paginated\) |
|
||||
| `hitsPerPage` | number | No | Number of indices per page \(default: 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `indices` | array | List of indices in the application |
|
||||
| ↳ `name` | string | Name of the index |
|
||||
| ↳ `entries` | number | Number of records in the index |
|
||||
| ↳ `dataSize` | number | Size of the index data in bytes |
|
||||
| ↳ `fileSize` | number | Size of the index files in bytes |
|
||||
| ↳ `lastBuildTimeS` | number | Last build duration in seconds |
|
||||
| ↳ `numberOfPendingTasks` | number | Number of pending indexing tasks |
|
||||
| ↳ `pendingTask` | boolean | Whether the index has pending tasks |
|
||||
| ↳ `createdAt` | string | Timestamp when the index was created |
|
||||
| ↳ `updatedAt` | string | Timestamp when the index was last updated |
|
||||
| ↳ `primary` | string | Name of the primary index \(if this is a replica\) |
|
||||
| ↳ `replicas` | array | List of replica index names |
|
||||
| ↳ `virtual` | boolean | Whether the index is a virtual replica |
|
||||
| `nbPages` | number | Total number of pages of indices |
|
||||
|
||||
### `algolia_get_settings`
|
||||
|
||||
Retrieve the settings of an Algolia index
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia API Key |
|
||||
| `indexName` | string | Yes | Name of the Algolia index |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `searchableAttributes` | array | List of searchable attributes |
|
||||
| `attributesForFaceting` | array | Attributes used for faceting |
|
||||
| `ranking` | array | Ranking criteria |
|
||||
| `customRanking` | array | Custom ranking criteria |
|
||||
| `replicas` | array | List of replica index names |
|
||||
| `hitsPerPage` | number | Default number of hits per page |
|
||||
| `maxValuesPerFacet` | number | Maximum number of facet values returned |
|
||||
| `highlightPreTag` | string | HTML tag inserted before highlighted parts |
|
||||
| `highlightPostTag` | string | HTML tag inserted after highlighted parts |
|
||||
| `paginationLimitedTo` | number | Maximum number of hits accessible via pagination |
|
||||
|
||||
### `algolia_update_settings`
|
||||
|
||||
Update the settings of an Algolia index
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia Admin API Key \(must have editSettings ACL\) |
|
||||
| `indexName` | string | Yes | Name of the Algolia index |
|
||||
| `settings` | json | Yes | JSON object with settings to update \(e.g., \{"searchableAttributes": \["name", "description"\], "customRanking": \["desc\(popularity\)"\]\}\) |
|
||||
| `forwardToReplicas` | boolean | No | Whether to apply changes to replica indices \(default: false\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskID` | number | Algolia task ID for tracking the settings update |
|
||||
| `updatedAt` | string | Timestamp when the settings were updated |
|
||||
|
||||
### `algolia_delete_index`
|
||||
|
||||
Delete an entire Algolia index and all its records
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia Admin API Key \(must have deleteIndex ACL\) |
|
||||
| `indexName` | string | Yes | Name of the Algolia index to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskID` | number | Algolia task ID for tracking the index deletion |
|
||||
| `deletedAt` | string | Timestamp when the index was deleted |
|
||||
|
||||
### `algolia_copy_move_index`
|
||||
|
||||
Copy or move an Algolia index to a new destination
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia Admin API Key |
|
||||
| `indexName` | string | Yes | Name of the source index |
|
||||
| `operation` | string | Yes | Operation to perform: "copy" or "move" |
|
||||
| `destination` | string | Yes | Name of the destination index |
|
||||
| `scope` | json | No | Array of scopes to copy \(only for "copy" operation\): \["settings", "synonyms", "rules"\]. Omit to copy everything including records. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskID` | number | Algolia task ID for tracking the copy/move operation |
|
||||
| `updatedAt` | string | Timestamp when the operation was performed |
|
||||
|
||||
### `algolia_clear_records`
|
||||
|
||||
Clear all records from an Algolia index while keeping settings, synonyms, and rules
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia Admin API Key \(must have deleteIndex ACL\) |
|
||||
| `indexName` | string | Yes | Name of the Algolia index to clear |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskID` | number | Algolia task ID for tracking the clear operation |
|
||||
| `updatedAt` | string | Timestamp when the records were cleared |
|
||||
|
||||
### `algolia_delete_by_filter`
|
||||
|
||||
Delete all records matching a filter from an Algolia index
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `applicationId` | string | Yes | Algolia Application ID |
|
||||
| `apiKey` | string | Yes | Algolia Admin API Key \(must have deleteIndex ACL\) |
|
||||
| `indexName` | string | Yes | Name of the Algolia index |
|
||||
| `filters` | string | No | Filter expression to match records for deletion \(e.g., "category:outdated"\) |
|
||||
| `facetFilters` | json | No | Array of facet filters \(e.g., \["brand:Acme"\]\) |
|
||||
| `numericFilters` | json | No | Array of numeric filters \(e.g., \["price > 100"\]\) |
|
||||
| `tagFilters` | json | No | Array of tag filters using the _tags attribute \(e.g., \["published"\]\) |
|
||||
| `aroundLatLng` | string | No | Coordinates for geo-search filter \(e.g., "40.71,-74.01"\) |
|
||||
| `aroundRadius` | number | No | Maximum radius in meters for geo-search, or "all" for unlimited |
|
||||
| `insideBoundingBox` | json | No | Bounding box coordinates as \[\[lat1, lng1, lat2, lng2\]\] for geo-search filter |
|
||||
| `insidePolygon` | json | No | Polygon coordinates as \[\[lat1, lng1, lat2, lng2, lat3, lng3, ...\]\] for geo-search filter |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskID` | number | Algolia task ID for tracking the delete-by-filter operation |
|
||||
| `updatedAt` | string | Timestamp when the operation was performed |
|
||||
|
||||
|
||||
1046
apps/docs/content/docs/en/tools/attio.mdx
Normal file
1046
apps/docs/content/docs/en/tools/attio.mdx
Normal file
File diff suppressed because it is too large
Load Diff
569
apps/docs/content/docs/en/tools/cloudflare.mdx
Normal file
569
apps/docs/content/docs/en/tools/cloudflare.mdx
Normal file
@@ -0,0 +1,569 @@
|
||||
---
|
||||
title: Cloudflare
|
||||
description: Manage DNS, domains, certificates, and cache
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="cloudflare"
|
||||
color="#F5F6FA"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Cloudflare](https://cloudflare.com/) is a global cloud platform that provides content delivery, domain management, cybersecurity, and performance services for websites and applications.
|
||||
|
||||
In Sim, the Cloudflare integration empowers your agents to automate the management of DNS records, SSL/TLS certificates, domains (zones), cache, zone settings, and more through easy-to-use API tools. Agents can securely list and edit domains, update DNS records, monitor analytics, and manage security and performance—all as part of your automated workflows.
|
||||
|
||||
With Cloudflare, you can:
|
||||
|
||||
- **Manage DNS and Domains**: List all your domains (zones), view zone details, and fully control DNS records from your automated agent workflows.
|
||||
- **Handle SSL/TLS Certificates and Settings**: Issue, renew, or list certificates and adjust security and performance settings for your sites.
|
||||
- **Purge Cache and Analyze Traffic**: Instantly purge edge cache and review real-time DNS analytics directly within your Sim agent processes.
|
||||
- **Automate Security and Operations**: Use agents to programmatically manage zones, update settings, and streamline repetitive Cloudflare tasks.
|
||||
|
||||
This integration enables streamlined, secure management of your site's infrastructure from within Sim. Your agents can integrate Cloudflare operations directly into processes—keeping DNS records up-to-date, responding to security events, improving site performance, and automating large-scale site and account administration.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Cloudflare into the workflow. Manage zones (domains), DNS records, SSL/TLS certificates, zone settings, DNS analytics, and cache purging via the Cloudflare API.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `cloudflare_list_zones`
|
||||
|
||||
Lists all zones (domains) in the Cloudflare account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `name` | string | No | Filter zones by domain name \(e.g., "example.com"\) |
|
||||
| `status` | string | No | Filter by zone status: "initializing", "pending", "active", or "moved" |
|
||||
| `page` | number | No | Page number for pagination \(default: 1\) |
|
||||
| `per_page` | number | No | Number of zones per page \(default: 20, max: 50\) |
|
||||
| `accountId` | string | No | Filter zones by account ID |
|
||||
| `order` | string | No | Sort field \(name, status, account.id, account.name\) |
|
||||
| `direction` | string | No | Sort direction \(asc, desc\) |
|
||||
| `match` | string | No | Match logic for filters \(any, all\). Default: all |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `zones` | array | List of zones/domains |
|
||||
| ↳ `id` | string | Zone ID |
|
||||
| ↳ `name` | string | Domain name |
|
||||
| ↳ `status` | string | Zone status \(initializing, pending, active, moved\) |
|
||||
| ↳ `paused` | boolean | Whether the zone is paused |
|
||||
| ↳ `type` | string | Zone type \(full, partial, or secondary\) |
|
||||
| ↳ `name_servers` | array | Assigned Cloudflare name servers |
|
||||
| ↳ `original_name_servers` | array | Original name servers before moving to Cloudflare |
|
||||
| ↳ `created_on` | string | ISO 8601 date when the zone was created |
|
||||
| ↳ `modified_on` | string | ISO 8601 date when the zone was last modified |
|
||||
| ↳ `activated_on` | string | ISO 8601 date when the zone was activated |
|
||||
| ↳ `development_mode` | number | Seconds remaining in development mode \(0 = off\) |
|
||||
| ↳ `plan` | object | Zone plan information |
|
||||
| ↳ `id` | string | Plan identifier |
|
||||
| ↳ `name` | string | Plan name |
|
||||
| ↳ `price` | number | Plan price |
|
||||
| ↳ `is_subscribed` | boolean | Whether the zone is subscribed to the plan |
|
||||
| ↳ `frequency` | string | Plan billing frequency |
|
||||
| ↳ `currency` | string | Plan currency |
|
||||
| ↳ `legacy_id` | string | Legacy plan identifier |
|
||||
| ↳ `account` | object | Account the zone belongs to |
|
||||
| ↳ `id` | string | Account identifier |
|
||||
| ↳ `name` | string | Account name |
|
||||
| ↳ `owner` | object | Zone owner information |
|
||||
| ↳ `id` | string | Owner identifier |
|
||||
| ↳ `name` | string | Owner name |
|
||||
| ↳ `type` | string | Owner type |
|
||||
| ↳ `meta` | object | Zone metadata |
|
||||
| ↳ `cdn_only` | boolean | Whether the zone is CDN only |
|
||||
| ↳ `custom_certificate_quota` | number | Custom certificate quota |
|
||||
| ↳ `dns_only` | boolean | Whether the zone is DNS only |
|
||||
| ↳ `foundation_dns` | boolean | Whether foundation DNS is enabled |
|
||||
| ↳ `page_rule_quota` | number | Page rule quota |
|
||||
| ↳ `phishing_detected` | boolean | Whether phishing was detected |
|
||||
| ↳ `step` | number | Current setup step |
|
||||
| ↳ `vanity_name_servers` | array | Custom vanity name servers |
|
||||
| ↳ `permissions` | array | User permissions for the zone |
|
||||
| `total_count` | number | Total number of zones matching the query |
|
||||
|
||||
### `cloudflare_get_zone`
|
||||
|
||||
Gets details for a specific zone (domain) by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to retrieve details for |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Zone ID |
|
||||
| `name` | string | Domain name |
|
||||
| `status` | string | Zone status \(initializing, pending, active, moved\) |
|
||||
| `paused` | boolean | Whether the zone is paused |
|
||||
| `type` | string | Zone type \(full, partial, or secondary\) |
|
||||
| `name_servers` | array | Assigned Cloudflare name servers |
|
||||
| `original_name_servers` | array | Original name servers before moving to Cloudflare |
|
||||
| `created_on` | string | ISO 8601 date when the zone was created |
|
||||
| `modified_on` | string | ISO 8601 date when the zone was last modified |
|
||||
| `activated_on` | string | ISO 8601 date when the zone was activated |
|
||||
| `development_mode` | number | Seconds remaining in development mode \(0 = off\) |
|
||||
| `plan` | object | Zone plan information |
|
||||
| ↳ `id` | string | Plan identifier |
|
||||
| ↳ `name` | string | Plan name |
|
||||
| ↳ `price` | number | Plan price |
|
||||
| ↳ `is_subscribed` | boolean | Whether the zone is subscribed to the plan |
|
||||
| ↳ `frequency` | string | Plan billing frequency |
|
||||
| ↳ `currency` | string | Plan currency |
|
||||
| ↳ `legacy_id` | string | Legacy plan identifier |
|
||||
| `account` | object | Account the zone belongs to |
|
||||
| ↳ `id` | string | Account identifier |
|
||||
| ↳ `name` | string | Account name |
|
||||
| `owner` | object | Zone owner information |
|
||||
| ↳ `id` | string | Owner identifier |
|
||||
| ↳ `name` | string | Owner name |
|
||||
| ↳ `type` | string | Owner type |
|
||||
| `meta` | object | Zone metadata |
|
||||
| ↳ `cdn_only` | boolean | Whether the zone is CDN only |
|
||||
| ↳ `custom_certificate_quota` | number | Custom certificate quota |
|
||||
| ↳ `dns_only` | boolean | Whether the zone is DNS only |
|
||||
| ↳ `foundation_dns` | boolean | Whether foundation DNS is enabled |
|
||||
| ↳ `page_rule_quota` | number | Page rule quota |
|
||||
| ↳ `phishing_detected` | boolean | Whether phishing was detected |
|
||||
| ↳ `step` | number | Current setup step |
|
||||
| `vanity_name_servers` | array | Custom vanity name servers |
|
||||
| `permissions` | array | User permissions for the zone |
|
||||
|
||||
### `cloudflare_create_zone`
|
||||
|
||||
Adds a new zone (domain) to the Cloudflare account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `name` | string | Yes | The domain name to add \(e.g., "example.com"\) |
|
||||
| `accountId` | string | Yes | The Cloudflare account ID |
|
||||
| `type` | string | No | Zone type: "full" \(Cloudflare manages DNS\), "partial" \(CNAME setup\), or "secondary" \(secondary DNS\) |
|
||||
| `jump_start` | boolean | No | Automatically attempt to fetch existing DNS records when creating the zone |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Created zone ID |
|
||||
| `name` | string | Domain name |
|
||||
| `status` | string | Zone status \(initializing, pending, active, moved\) |
|
||||
| `paused` | boolean | Whether the zone is paused |
|
||||
| `type` | string | Zone type \(full, partial, or secondary\) |
|
||||
| `name_servers` | array | Assigned Cloudflare name servers |
|
||||
| `original_name_servers` | array | Original name servers before moving to Cloudflare |
|
||||
| `created_on` | string | ISO 8601 date when the zone was created |
|
||||
| `modified_on` | string | ISO 8601 date when the zone was last modified |
|
||||
| `activated_on` | string | ISO 8601 date when the zone was activated |
|
||||
| `development_mode` | number | Seconds remaining in development mode \(0 = off\) |
|
||||
| `plan` | object | Zone plan information |
|
||||
| ↳ `id` | string | Plan identifier |
|
||||
| ↳ `name` | string | Plan name |
|
||||
| ↳ `price` | number | Plan price |
|
||||
| ↳ `is_subscribed` | boolean | Whether the zone is subscribed to the plan |
|
||||
| ↳ `frequency` | string | Plan billing frequency |
|
||||
| ↳ `currency` | string | Plan currency |
|
||||
| ↳ `legacy_id` | string | Legacy plan identifier |
|
||||
| `account` | object | Account the zone belongs to |
|
||||
| ↳ `id` | string | Account identifier |
|
||||
| ↳ `name` | string | Account name |
|
||||
| `owner` | object | Zone owner information |
|
||||
| ↳ `id` | string | Owner identifier |
|
||||
| ↳ `name` | string | Owner name |
|
||||
| ↳ `type` | string | Owner type |
|
||||
| `meta` | object | Zone metadata |
|
||||
| ↳ `cdn_only` | boolean | Whether the zone is CDN only |
|
||||
| ↳ `custom_certificate_quota` | number | Custom certificate quota |
|
||||
| ↳ `dns_only` | boolean | Whether the zone is DNS only |
|
||||
| ↳ `foundation_dns` | boolean | Whether foundation DNS is enabled |
|
||||
| ↳ `page_rule_quota` | number | Page rule quota |
|
||||
| ↳ `phishing_detected` | boolean | Whether phishing was detected |
|
||||
| ↳ `step` | number | Current setup step |
|
||||
| `vanity_name_servers` | array | Custom vanity name servers |
|
||||
| `permissions` | array | User permissions for the zone |
|
||||
|
||||
### `cloudflare_delete_zone`
|
||||
|
||||
Deletes a zone (domain) from the Cloudflare account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to delete |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Deleted zone ID |
|
||||
|
||||
### `cloudflare_list_dns_records`
|
||||
|
||||
Lists DNS records for a specific zone.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to list DNS records for |
|
||||
| `type` | string | No | Filter by record type \(e.g., "A", "AAAA", "CNAME", "MX", "TXT"\) |
|
||||
| `name` | string | No | Filter by record name \(exact match\) |
|
||||
| `content` | string | No | Filter by record content \(exact match\) |
|
||||
| `page` | number | No | Page number for pagination \(default: 1\) |
|
||||
| `per_page` | number | No | Number of records per page \(default: 100, max: 5000000\) |
|
||||
| `direction` | string | No | Sort direction \(asc or desc\) |
|
||||
| `match` | string | No | Match logic for filters: any or all \(default: all\) |
|
||||
| `order` | string | No | Sort field \(type, name, content, ttl, proxied\) |
|
||||
| `proxied` | boolean | No | Filter by proxy status |
|
||||
| `search` | string | No | Free-text search across record name, content, and value |
|
||||
| `tag` | string | No | Filter by tags \(comma-separated\) |
|
||||
| `tag_match` | string | No | Tag filter match logic: any or all |
|
||||
| `commentFilter` | string | No | Filter records by comment content \(substring match\) |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | List of DNS records |
|
||||
| ↳ `id` | string | Unique identifier for the DNS record |
|
||||
| ↳ `zone_id` | string | The ID of the zone the record belongs to |
|
||||
| ↳ `zone_name` | string | The name of the zone |
|
||||
| ↳ `type` | string | Record type \(A, AAAA, CNAME, MX, TXT, etc.\) |
|
||||
| ↳ `name` | string | Record name \(e.g., example.com\) |
|
||||
| ↳ `content` | string | Record content \(e.g., IP address\) |
|
||||
| ↳ `proxiable` | boolean | Whether the record can be proxied |
|
||||
| ↳ `proxied` | boolean | Whether Cloudflare proxy is enabled |
|
||||
| ↳ `ttl` | number | TTL in seconds \(1 = automatic\) |
|
||||
| ↳ `locked` | boolean | Whether the record is locked |
|
||||
| ↳ `priority` | number | MX/SRV record priority |
|
||||
| ↳ `comment` | string | Comment associated with the record |
|
||||
| ↳ `tags` | array | Tags associated with the record |
|
||||
| ↳ `comment_modified_on` | string | ISO 8601 timestamp when the comment was last modified |
|
||||
| ↳ `tags_modified_on` | string | ISO 8601 timestamp when tags were last modified |
|
||||
| ↳ `meta` | object | Record metadata |
|
||||
| ↳ `source` | string | Source of the DNS record |
|
||||
| ↳ `created_on` | string | ISO 8601 timestamp when the record was created |
|
||||
| ↳ `modified_on` | string | ISO 8601 timestamp when the record was last modified |
|
||||
| `total_count` | number | Total number of DNS records matching the query |
|
||||
|
||||
### `cloudflare_create_dns_record`
|
||||
|
||||
Creates a new DNS record for a zone.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to create the DNS record in |
|
||||
| `type` | string | Yes | DNS record type \(e.g., "A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV"\) |
|
||||
| `name` | string | Yes | DNS record name \(e.g., "example.com" or "subdomain.example.com"\) |
|
||||
| `content` | string | Yes | DNS record content \(e.g., IP address for A records, target for CNAME\) |
|
||||
| `ttl` | number | No | Time to live in seconds \(1 = automatic, default: 1\) |
|
||||
| `proxied` | boolean | No | Whether to enable Cloudflare proxy \(default: false\) |
|
||||
| `priority` | number | No | Priority for MX and SRV records |
|
||||
| `comment` | string | No | Comment for the DNS record |
|
||||
| `tags` | string | No | Comma-separated tags for the DNS record |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Unique identifier for the created DNS record |
|
||||
| `zone_id` | string | The ID of the zone the record belongs to |
|
||||
| `zone_name` | string | The name of the zone |
|
||||
| `type` | string | DNS record type \(A, AAAA, CNAME, MX, TXT, etc.\) |
|
||||
| `name` | string | DNS record hostname |
|
||||
| `content` | string | DNS record value \(e.g., IP address, target hostname\) |
|
||||
| `proxiable` | boolean | Whether the record can be proxied through Cloudflare |
|
||||
| `proxied` | boolean | Whether Cloudflare proxy is enabled |
|
||||
| `ttl` | number | Time to live in seconds \(1 = automatic\) |
|
||||
| `locked` | boolean | Whether the record is locked |
|
||||
| `priority` | number | Priority for MX and SRV records |
|
||||
| `comment` | string | Comment associated with the record |
|
||||
| `tags` | array | Tags associated with the record |
|
||||
| `comment_modified_on` | string | ISO 8601 timestamp when the comment was last modified |
|
||||
| `tags_modified_on` | string | ISO 8601 timestamp when tags were last modified |
|
||||
| `meta` | object | Record metadata |
|
||||
| ↳ `source` | string | Source of the DNS record |
|
||||
| `created_on` | string | ISO 8601 timestamp when the record was created |
|
||||
| `modified_on` | string | ISO 8601 timestamp when the record was last modified |
|
||||
|
||||
### `cloudflare_update_dns_record`
|
||||
|
||||
Updates an existing DNS record for a zone.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID containing the DNS record |
|
||||
| `recordId` | string | Yes | The DNS record ID to update |
|
||||
| `type` | string | No | DNS record type \(e.g., "A", "AAAA", "CNAME", "MX", "TXT"\) |
|
||||
| `name` | string | No | DNS record name |
|
||||
| `content` | string | No | DNS record content \(e.g., IP address\) |
|
||||
| `ttl` | number | No | Time to live in seconds \(1 = automatic\) |
|
||||
| `proxied` | boolean | No | Whether to enable Cloudflare proxy |
|
||||
| `priority` | number | No | Priority for MX and SRV records |
|
||||
| `comment` | string | No | Comment for the DNS record |
|
||||
| `tags` | string | No | Comma-separated tags for the DNS record |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Unique identifier for the updated DNS record |
|
||||
| `zone_id` | string | The ID of the zone the record belongs to |
|
||||
| `zone_name` | string | The name of the zone |
|
||||
| `type` | string | DNS record type \(A, AAAA, CNAME, MX, TXT, etc.\) |
|
||||
| `name` | string | DNS record hostname |
|
||||
| `content` | string | DNS record value \(e.g., IP address, target hostname\) |
|
||||
| `proxiable` | boolean | Whether the record can be proxied through Cloudflare |
|
||||
| `proxied` | boolean | Whether Cloudflare proxy is enabled |
|
||||
| `ttl` | number | Time to live in seconds \(1 = automatic\) |
|
||||
| `locked` | boolean | Whether the record is locked |
|
||||
| `priority` | number | Priority for MX and SRV records |
|
||||
| `comment` | string | Comment associated with the record |
|
||||
| `tags` | array | Tags associated with the record |
|
||||
| `comment_modified_on` | string | ISO 8601 timestamp when the comment was last modified |
|
||||
| `tags_modified_on` | string | ISO 8601 timestamp when tags were last modified |
|
||||
| `meta` | object | Record metadata |
|
||||
| ↳ `source` | string | Source of the DNS record |
|
||||
| `created_on` | string | ISO 8601 timestamp when the record was created |
|
||||
| `modified_on` | string | ISO 8601 timestamp when the record was last modified |
|
||||
|
||||
### `cloudflare_delete_dns_record`
|
||||
|
||||
Deletes a DNS record from a zone.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID containing the DNS record |
|
||||
| `recordId` | string | Yes | The DNS record ID to delete |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Deleted record ID |
|
||||
|
||||
### `cloudflare_list_certificates`
|
||||
|
||||
Lists SSL/TLS certificate packs for a zone.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to list certificates for |
|
||||
| `status` | string | No | Filter certificate packs by status \(e.g., "all", "active", "pending"\) |
|
||||
| `page` | number | No | Page number of paginated results \(default: 1\) |
|
||||
| `per_page` | number | No | Number of certificate packs per page \(default: 20, min: 5, max: 50\) |
|
||||
| `deploy` | string | No | Filter by deployment environment: "staging" or "production" |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `certificates` | array | List of SSL/TLS certificate packs |
|
||||
| ↳ `id` | string | Certificate pack ID |
|
||||
| ↳ `type` | string | Certificate type \(e.g., "universal", "advanced"\) |
|
||||
| ↳ `hosts` | array | Hostnames covered by this certificate pack |
|
||||
| ↳ `primary_certificate` | string | ID of the primary certificate in the pack |
|
||||
| ↳ `status` | string | Certificate pack status \(e.g., "active", "pending"\) |
|
||||
| ↳ `certificates` | array | Individual certificates within the pack |
|
||||
| ↳ `id` | string | Certificate ID |
|
||||
| ↳ `hosts` | array | Hostnames covered by this certificate |
|
||||
| ↳ `issuer` | string | Certificate issuer |
|
||||
| ↳ `signature` | string | Signature algorithm \(e.g., "ECDSAWithSHA256"\) |
|
||||
| ↳ `status` | string | Certificate status |
|
||||
| ↳ `bundle_method` | string | Bundle method \(e.g., "ubiquitous"\) |
|
||||
| ↳ `zone_id` | string | Zone ID the certificate belongs to |
|
||||
| ↳ `uploaded_on` | string | Upload date \(ISO 8601\) |
|
||||
| ↳ `modified_on` | string | Last modified date \(ISO 8601\) |
|
||||
| ↳ `expires_on` | string | Expiration date \(ISO 8601\) |
|
||||
| ↳ `priority` | number | Certificate priority order |
|
||||
| ↳ `geo_restrictions` | object | Geographic restrictions for the certificate |
|
||||
| ↳ `label` | string | Geographic restriction label |
|
||||
| ↳ `cloudflare_branding` | boolean | Whether Cloudflare branding is enabled on the certificate |
|
||||
| ↳ `validation_method` | string | Validation method \(e.g., "txt", "http", "cname"\) |
|
||||
| ↳ `validity_days` | number | Validity period in days |
|
||||
| ↳ `certificate_authority` | string | Certificate authority \(e.g., "lets_encrypt", "google"\) |
|
||||
| ↳ `validation_errors` | array | Validation issues for the certificate pack |
|
||||
| ↳ `message` | string | Validation error message |
|
||||
| ↳ `validation_records` | array | Validation records for the certificate pack |
|
||||
| ↳ `cname` | string | CNAME record name |
|
||||
| ↳ `cname_target` | string | CNAME record target |
|
||||
| ↳ `emails` | array | Email addresses for validation |
|
||||
| ↳ `http_body` | string | HTTP validation body content |
|
||||
| ↳ `http_url` | string | HTTP validation URL |
|
||||
| ↳ `status` | string | Validation record status |
|
||||
| ↳ `txt_name` | string | TXT record name |
|
||||
| ↳ `txt_value` | string | TXT record value |
|
||||
| ↳ `dcv_delegation_records` | array | Domain control validation delegation records |
|
||||
| ↳ `cname` | string | CNAME record name |
|
||||
| ↳ `cname_target` | string | CNAME record target |
|
||||
| ↳ `emails` | array | Email addresses for validation |
|
||||
| ↳ `http_body` | string | HTTP validation body content |
|
||||
| ↳ `http_url` | string | HTTP validation URL |
|
||||
| ↳ `status` | string | Delegation record status |
|
||||
| ↳ `txt_name` | string | TXT record name |
|
||||
| ↳ `txt_value` | string | TXT record value |
|
||||
| `total_count` | number | Total number of certificate packs |
|
||||
|
||||
### `cloudflare_get_zone_settings`
|
||||
|
||||
Gets all settings for a zone including SSL mode, minification, caching level, and security settings.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to get settings for |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `settings` | array | List of zone settings |
|
||||
| ↳ `id` | string | Setting identifier \(e.g., ssl, minify, cache_level, security_level, always_use_https\) |
|
||||
| ↳ `value` | string | Setting value as a string. Simple values returned as-is \(e.g., "full", "on"\). Complex values are JSON-stringified \(e.g., \ |
|
||||
| ↳ `editable` | boolean | Whether the setting can be modified for the current zone plan |
|
||||
| ↳ `modified_on` | string | ISO 8601 timestamp when the setting was last modified |
|
||||
| ↳ `time_remaining` | number | Seconds remaining until the setting can be modified again \(only present for rate-limited settings\) |
|
||||
|
||||
### `cloudflare_update_zone_setting`
|
||||
|
||||
Updates a specific zone setting such as SSL mode, security level, cache level, minification, or other configuration.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to update settings for |
|
||||
| `settingId` | string | Yes | Setting to update \(e.g., "ssl", "security_level", "cache_level", "minify", "always_use_https", "browser_cache_ttl", "http3", "min_tls_version", "ciphers"\) |
|
||||
| `value` | string | Yes | New value for the setting as a string or JSON string for complex values \(e.g., "full" for SSL, "medium" for security_level, "aggressive" for cache_level, \'\{"css":"on","html":"on","js":"on"\}\' for minify, \'\["ECDHE-RSA-AES128-GCM-SHA256"\]\' for ciphers\) |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Setting identifier \(e.g., ssl, minify, cache_level\) |
|
||||
| `value` | string | Updated setting value as a string. Simple values returned as-is \(e.g., "full", "on"\). Complex values are JSON-stringified. |
|
||||
| `editable` | boolean | Whether the setting can be modified for the current zone plan |
|
||||
| `modified_on` | string | ISO 8601 timestamp when the setting was last modified |
|
||||
| `time_remaining` | number | Seconds remaining until the setting can be modified again \(only present for rate-limited settings\) |
|
||||
|
||||
### `cloudflare_dns_analytics`
|
||||
|
||||
Gets DNS analytics report for a zone including query counts and trends.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to get DNS analytics for |
|
||||
| `since` | string | No | Start date for analytics \(ISO 8601, e.g., "2024-01-01T00:00:00Z"\) or relative \(e.g., "-6h"\) |
|
||||
| `until` | string | No | End date for analytics \(ISO 8601, e.g., "2024-01-31T23:59:59Z"\) or relative \(e.g., "now"\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics to retrieve \(e.g., "queryCount,uncachedCount,staleCount,responseTimeAvg,responseTimeMedian,responseTime90th,responseTime99th"\) |
|
||||
| `dimensions` | string | No | Comma-separated dimensions to group by \(e.g., "queryName,queryType,responseCode,responseCached,coloName,origin,dayOfWeek,tcp,ipVersion,querySizeBucket,responseSizeBucket"\) |
|
||||
| `filters` | string | No | Filters to apply to the data \(e.g., "queryType==A"\) |
|
||||
| `sort` | string | No | Sort order for the result set. Fields must be included in metrics or dimensions \(e.g., "+queryCount" or "-responseTimeAvg"\) |
|
||||
| `limit` | number | No | Maximum number of results to return |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totals` | object | Aggregate DNS analytics totals for the entire queried period |
|
||||
| ↳ `queryCount` | number | Total number of DNS queries |
|
||||
| ↳ `uncachedCount` | number | Number of uncached DNS queries |
|
||||
| ↳ `staleCount` | number | Number of stale DNS queries |
|
||||
| ↳ `responseTimeAvg` | number | Average response time in milliseconds |
|
||||
| ↳ `responseTimeMedian` | number | Median response time in milliseconds |
|
||||
| ↳ `responseTime90th` | number | 90th percentile response time in milliseconds |
|
||||
| ↳ `responseTime99th` | number | 99th percentile response time in milliseconds |
|
||||
| `min` | object | Minimum values across the analytics period |
|
||||
| ↳ `queryCount` | number | Minimum number of DNS queries |
|
||||
| ↳ `uncachedCount` | number | Minimum number of uncached DNS queries |
|
||||
| ↳ `staleCount` | number | Minimum number of stale DNS queries |
|
||||
| ↳ `responseTimeAvg` | number | Minimum average response time in milliseconds |
|
||||
| ↳ `responseTimeMedian` | number | Minimum median response time in milliseconds |
|
||||
| ↳ `responseTime90th` | number | Minimum 90th percentile response time in milliseconds |
|
||||
| ↳ `responseTime99th` | number | Minimum 99th percentile response time in milliseconds |
|
||||
| `max` | object | Maximum values across the analytics period |
|
||||
| ↳ `queryCount` | number | Maximum number of DNS queries |
|
||||
| ↳ `uncachedCount` | number | Maximum number of uncached DNS queries |
|
||||
| ↳ `staleCount` | number | Maximum number of stale DNS queries |
|
||||
| ↳ `responseTimeAvg` | number | Maximum average response time in milliseconds |
|
||||
| ↳ `responseTimeMedian` | number | Maximum median response time in milliseconds |
|
||||
| ↳ `responseTime90th` | number | Maximum 90th percentile response time in milliseconds |
|
||||
| ↳ `responseTime99th` | number | Maximum 99th percentile response time in milliseconds |
|
||||
| `data` | array | Raw analytics data rows returned by the Cloudflare DNS analytics report |
|
||||
| ↳ `dimensions` | array | Dimension values for this data row, parallel to the requested dimensions list |
|
||||
| ↳ `metrics` | array | Metric values for this data row, parallel to the requested metrics list |
|
||||
| `data_lag` | number | Processing lag in seconds before analytics data becomes available |
|
||||
| `rows` | number | Total number of rows in the result set |
|
||||
| `query` | object | Echo of the query parameters sent to the API |
|
||||
| ↳ `since` | string | Start date of the analytics query |
|
||||
| ↳ `until` | string | End date of the analytics query |
|
||||
| ↳ `metrics` | array | Metrics requested in the query |
|
||||
| ↳ `dimensions` | array | Dimensions requested in the query |
|
||||
| ↳ `filters` | string | Filters applied to the query |
|
||||
| ↳ `sort` | array | Sort order applied to the query |
|
||||
| ↳ `limit` | number | Maximum number of results requested |
|
||||
|
||||
### `cloudflare_purge_cache`
|
||||
|
||||
Purges cached content for a zone. Can purge everything or specific files/tags/hosts/prefixes.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to purge cache for |
|
||||
| `purge_everything` | boolean | No | Set to true to purge all cached content. Mutually exclusive with files, tags, hosts, and prefixes |
|
||||
| `files` | string | No | Comma-separated list of URLs to purge from cache |
|
||||
| `tags` | string | No | Comma-separated list of cache tags to purge \(Enterprise only\) |
|
||||
| `hosts` | string | No | Comma-separated list of hostnames to purge \(Enterprise only\) |
|
||||
| `prefixes` | string | No | Comma-separated list of URL prefixes to purge \(Enterprise only\) |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Purge request identifier returned by Cloudflare |
|
||||
|
||||
|
||||
@@ -326,6 +326,8 @@ Get details about a specific version of a Confluence page.
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `pageId` | string | ID of the page |
|
||||
| `title` | string | Page title at this version |
|
||||
| `content` | string | Page content with HTML tags stripped at this version |
|
||||
| `version` | object | Detailed version information |
|
||||
| ↳ `number` | number | Version number |
|
||||
| ↳ `message` | string | Version message |
|
||||
@@ -336,6 +338,9 @@ Get details about a specific version of a Confluence page.
|
||||
| ↳ `collaborators` | array | List of collaborator account IDs for this version |
|
||||
| ↳ `prevVersion` | number | Previous version number |
|
||||
| ↳ `nextVersion` | number | Next version number |
|
||||
| `body` | object | Raw page body content in storage format at this version |
|
||||
| ↳ `value` | string | The content value in the specified format |
|
||||
| ↳ `representation` | string | Content representation type |
|
||||
|
||||
### `confluence_list_page_properties`
|
||||
|
||||
@@ -1008,6 +1013,85 @@ Get details about a specific Confluence space.
|
||||
| ↳ `value` | string | Description text content |
|
||||
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
||||
|
||||
### `confluence_create_space`
|
||||
|
||||
Create a new Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `name` | string | Yes | Name for the new space |
|
||||
| `key` | string | Yes | Unique key for the space \(uppercase, no spaces\) |
|
||||
| `description` | string | No | Description for the new space |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `spaceId` | string | Created space ID |
|
||||
| `name` | string | Space name |
|
||||
| `key` | string | Space key |
|
||||
| `type` | string | Space type |
|
||||
| `status` | string | Space status |
|
||||
| `url` | string | URL to view the space |
|
||||
| `homepageId` | string | Homepage ID |
|
||||
| `description` | object | Space description |
|
||||
| ↳ `value` | string | Description text content |
|
||||
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
||||
|
||||
### `confluence_update_space`
|
||||
|
||||
Update a Confluence space name or description.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | ID of the space to update |
|
||||
| `name` | string | No | New name for the space |
|
||||
| `description` | string | No | New description for the space |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `spaceId` | string | Updated space ID |
|
||||
| `name` | string | Space name |
|
||||
| `key` | string | Space key |
|
||||
| `type` | string | Space type |
|
||||
| `status` | string | Space status |
|
||||
| `url` | string | URL to view the space |
|
||||
| `description` | object | Space description |
|
||||
| ↳ `value` | string | Description text content |
|
||||
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
||||
|
||||
### `confluence_delete_space`
|
||||
|
||||
Delete a Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | ID of the space to delete |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `spaceId` | string | Deleted space ID |
|
||||
| `deleted` | boolean | Deletion status |
|
||||
|
||||
### `confluence_list_spaces`
|
||||
|
||||
List all Confluence spaces accessible to the user.
|
||||
@@ -1040,4 +1124,311 @@ List all Confluence spaces accessible to the user.
|
||||
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
|
||||
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||
|
||||
### `confluence_list_space_properties`
|
||||
|
||||
List properties on a Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | Space ID to list properties for |
|
||||
| `limit` | number | No | Maximum number of properties to return \(default: 50, max: 250\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `properties` | array | Array of space properties |
|
||||
| ↳ `id` | string | Property ID |
|
||||
| ↳ `key` | string | Property key |
|
||||
| ↳ `value` | json | Property value |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||
|
||||
### `confluence_create_space_property`
|
||||
|
||||
Create a property on a Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | Space ID to create the property on |
|
||||
| `key` | string | Yes | Property key/name |
|
||||
| `value` | json | No | Property value \(JSON\) |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `propertyId` | string | Created property ID |
|
||||
| `key` | string | Property key |
|
||||
| `value` | json | Property value |
|
||||
| `spaceId` | string | Space ID |
|
||||
|
||||
### `confluence_delete_space_property`
|
||||
|
||||
Delete a property from a Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | Space ID the property belongs to |
|
||||
| `propertyId` | string | Yes | Property ID to delete |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `propertyId` | string | Deleted property ID |
|
||||
| `deleted` | boolean | Deletion status |
|
||||
|
||||
### `confluence_list_space_permissions`
|
||||
|
||||
List permissions for a Confluence space.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `spaceId` | string | Yes | Space ID to list permissions for |
|
||||
| `limit` | number | No | Maximum number of permissions to return \(default: 50, max: 250\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `permissions` | array | Array of space permissions |
|
||||
| ↳ `id` | string | Permission ID |
|
||||
| ↳ `principalType` | string | Principal type \(user, group, role\) |
|
||||
| ↳ `principalId` | string | Principal ID |
|
||||
| ↳ `operationKey` | string | Operation key \(read, create, delete, etc.\) |
|
||||
| ↳ `operationTargetType` | string | Target type \(page, blogpost, space, etc.\) |
|
||||
| ↳ `anonymousAccess` | boolean | Whether anonymous access is allowed |
|
||||
| ↳ `unlicensedAccess` | boolean | Whether unlicensed access is allowed |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||
|
||||
### `confluence_get_page_descendants`
|
||||
|
||||
Get all descendants of a Confluence page recursively.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `pageId` | string | Yes | Page ID to get descendants for |
|
||||
| `limit` | number | No | Maximum number of descendants to return \(default: 50, max: 250\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `descendants` | array | Array of descendant pages |
|
||||
| ↳ `id` | string | Page ID |
|
||||
| ↳ `title` | string | Page title |
|
||||
| ↳ `type` | string | Content type \(page, whiteboard, database, etc.\) |
|
||||
| ↳ `status` | string | Page status |
|
||||
| ↳ `spaceId` | string | Space ID |
|
||||
| ↳ `parentId` | string | Parent page ID |
|
||||
| ↳ `childPosition` | number | Position among siblings |
|
||||
| ↳ `depth` | number | Depth in the hierarchy |
|
||||
| `pageId` | string | Parent page ID |
|
||||
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||
|
||||
### `confluence_list_tasks`
|
||||
|
||||
List inline tasks from Confluence. Optionally filter by page, space, assignee, or status.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `pageId` | string | No | Filter tasks by page ID |
|
||||
| `spaceId` | string | No | Filter tasks by space ID |
|
||||
| `assignedTo` | string | No | Filter tasks by assignee account ID |
|
||||
| `status` | string | No | Filter tasks by status \(complete or incomplete\) |
|
||||
| `limit` | number | No | Maximum number of tasks to return \(default: 50, max: 250\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `tasks` | array | Array of Confluence tasks |
|
||||
| ↳ `id` | string | Task ID |
|
||||
| ↳ `localId` | string | Local task ID |
|
||||
| ↳ `spaceId` | string | Space ID |
|
||||
| ↳ `pageId` | string | Page ID |
|
||||
| ↳ `blogPostId` | string | Blog post ID |
|
||||
| ↳ `status` | string | Task status \(complete or incomplete\) |
|
||||
| ↳ `body` | string | Task body content in storage format |
|
||||
| ↳ `createdBy` | string | Creator account ID |
|
||||
| ↳ `assignedTo` | string | Assignee account ID |
|
||||
| ↳ `completedBy` | string | Completer account ID |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
| ↳ `dueAt` | string | Due date |
|
||||
| ↳ `completedAt` | string | Completion timestamp |
|
||||
| `nextCursor` | string | Cursor for fetching the next page of results |
|
||||
|
||||
### `confluence_get_task`
|
||||
|
||||
Get a specific Confluence inline task by ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `taskId` | string | Yes | The ID of the task to retrieve |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `id` | string | Task ID |
|
||||
| `localId` | string | Local task ID |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `pageId` | string | Page ID |
|
||||
| `blogPostId` | string | Blog post ID |
|
||||
| `status` | string | Task status \(complete or incomplete\) |
|
||||
| `body` | string | Task body content in storage format |
|
||||
| `createdBy` | string | Creator account ID |
|
||||
| `assignedTo` | string | Assignee account ID |
|
||||
| `completedBy` | string | Completer account ID |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last update timestamp |
|
||||
| `dueAt` | string | Due date |
|
||||
| `completedAt` | string | Completion timestamp |
|
||||
|
||||
### `confluence_update_task`
|
||||
|
||||
Update the status of a Confluence inline task (complete or incomplete).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `taskId` | string | Yes | The ID of the task to update |
|
||||
| `status` | string | Yes | New status for the task \(complete or incomplete\) |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `id` | string | Task ID |
|
||||
| `localId` | string | Local task ID |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `pageId` | string | Page ID |
|
||||
| `blogPostId` | string | Blog post ID |
|
||||
| `status` | string | Updated task status |
|
||||
| `body` | string | Task body content in storage format |
|
||||
| `createdBy` | string | Creator account ID |
|
||||
| `assignedTo` | string | Assignee account ID |
|
||||
| `completedBy` | string | Completer account ID |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last update timestamp |
|
||||
| `dueAt` | string | Due date |
|
||||
| `completedAt` | string | Completion timestamp |
|
||||
|
||||
### `confluence_update_blogpost`
|
||||
|
||||
Update an existing Confluence blog post title and/or content.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `blogPostId` | string | Yes | The ID of the blog post to update |
|
||||
| `title` | string | No | New title for the blog post |
|
||||
| `content` | string | No | New content for the blog post in storage format |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `blogPostId` | string | Updated blog post ID |
|
||||
| `title` | string | Blog post title |
|
||||
| `status` | string | Blog post status |
|
||||
| `spaceId` | string | Space ID |
|
||||
| `version` | json | Version information |
|
||||
| `url` | string | URL to view the blog post |
|
||||
|
||||
### `confluence_delete_blogpost`
|
||||
|
||||
Delete a Confluence blog post.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `blogPostId` | string | Yes | The ID of the blog post to delete |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `blogPostId` | string | Deleted blog post ID |
|
||||
| `deleted` | boolean | Deletion status |
|
||||
|
||||
### `confluence_get_user`
|
||||
|
||||
Get display name and profile info for a Confluence user by account ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `accountId` | string | Yes | The Atlassian account ID of the user to look up |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | ISO 8601 timestamp of the operation |
|
||||
| `accountId` | string | Atlassian account ID of the user |
|
||||
| `displayName` | string | Display name of the user |
|
||||
| `email` | string | Email address of the user |
|
||||
| `accountType` | string | Account type \(e.g., atlassian, app, customer\) |
|
||||
| `profilePicture` | string | Path to the user profile picture |
|
||||
| `publicName` | string | Public name of the user |
|
||||
|
||||
|
||||
|
||||
157
apps/docs/content/docs/en/tools/devin.mdx
Normal file
157
apps/docs/content/docs/en/tools/devin.mdx
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
title: Devin
|
||||
description: Autonomous AI software engineer
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="devin"
|
||||
color="#12141A"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Devin](https://devin.ai/) is an autonomous AI software engineer by Cognition that can independently write, run, debug, and deploy code.
|
||||
|
||||
With Devin, you can:
|
||||
|
||||
- **Automate coding tasks**: Assign software engineering tasks and let Devin autonomously write, test, and iterate on code
|
||||
- **Manage sessions**: Create, monitor, and interact with Devin sessions to track progress on assigned tasks
|
||||
- **Guide active work**: Send messages to running sessions to provide additional context, redirect efforts, or answer questions
|
||||
- **Retrieve structured output**: Poll completed sessions for pull requests, structured results, and detailed status
|
||||
- **Control costs**: Set ACU (Autonomous Compute Unit) limits to cap spending on long-running tasks
|
||||
- **Standardize workflows**: Use playbook IDs to apply repeatable task patterns across sessions
|
||||
|
||||
In Sim, the Devin integration enables your agents to programmatically manage Devin sessions as part of their workflows:
|
||||
|
||||
- **Create sessions**: Kick off new Devin sessions with a prompt describing the task, optional playbook, ACU limits, and tags
|
||||
- **Get session details**: Retrieve the full state of a session including status, pull requests, structured output, and resource consumption
|
||||
- **List sessions**: Query all sessions in your organization with optional pagination
|
||||
- **Send messages**: Communicate with active or suspended sessions to provide guidance, and automatically resume suspended sessions
|
||||
|
||||
This allows for powerful automation scenarios such as triggering code generation from upstream events, polling for completion before consuming results, orchestrating multi-step development pipelines, and integrating Devin's output into broader agent workflows.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Devin into your workflow. Create sessions to assign coding tasks, send messages to guide active sessions, and retrieve session status and results. Devin autonomously writes, runs, and tests code.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `devin_create_session`
|
||||
|
||||
Create a new Devin session with a prompt. Devin will autonomously work on the task described in the prompt.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
|
||||
| `prompt` | string | Yes | The task prompt for Devin to work on |
|
||||
| `playbookId` | string | No | Optional playbook ID to guide the session |
|
||||
| `maxAcuLimit` | number | No | Maximum ACU limit for the session |
|
||||
| `tags` | string | No | Comma-separated tags for the session |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sessionId` | string | Unique identifier for the session |
|
||||
| `url` | string | URL to view the session in the Devin UI |
|
||||
| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) |
|
||||
| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) |
|
||||
| `title` | string | Session title |
|
||||
| `createdAt` | number | Unix timestamp when the session was created |
|
||||
| `updatedAt` | number | Unix timestamp when the session was last updated |
|
||||
| `acusConsumed` | number | ACUs consumed by the session |
|
||||
| `tags` | json | Tags associated with the session |
|
||||
| `pullRequests` | json | Pull requests created during the session |
|
||||
| `structuredOutput` | json | Structured output from the session |
|
||||
| `playbookId` | string | Associated playbook ID |
|
||||
|
||||
### `devin_get_session`
|
||||
|
||||
Retrieve details of an existing Devin session including status, tags, pull requests, and structured output.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
|
||||
| `sessionId` | string | Yes | The session ID to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sessionId` | string | Unique identifier for the session |
|
||||
| `url` | string | URL to view the session in the Devin UI |
|
||||
| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) |
|
||||
| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) |
|
||||
| `title` | string | Session title |
|
||||
| `createdAt` | number | Unix timestamp when the session was created |
|
||||
| `updatedAt` | number | Unix timestamp when the session was last updated |
|
||||
| `acusConsumed` | number | ACUs consumed by the session |
|
||||
| `tags` | json | Tags associated with the session |
|
||||
| `pullRequests` | json | Pull requests created during the session |
|
||||
| `structuredOutput` | json | Structured output from the session |
|
||||
| `playbookId` | string | Associated playbook ID |
|
||||
|
||||
### `devin_list_sessions`
|
||||
|
||||
List Devin sessions in the organization. Returns up to 100 sessions by default.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
|
||||
| `limit` | number | No | Maximum number of sessions to return \(1-200, default: 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sessions` | array | List of Devin sessions |
|
||||
| ↳ `sessionId` | string | Unique identifier for the session |
|
||||
| ↳ `url` | string | URL to view the session |
|
||||
| ↳ `status` | string | Session status |
|
||||
| ↳ `statusDetail` | string | Detailed status |
|
||||
| ↳ `title` | string | Session title |
|
||||
| ↳ `createdAt` | number | Creation timestamp \(Unix\) |
|
||||
| ↳ `updatedAt` | number | Last updated timestamp \(Unix\) |
|
||||
| ↳ `tags` | json | Session tags |
|
||||
|
||||
### `devin_send_message`
|
||||
|
||||
Send a message to a Devin session. If the session is suspended, it will be automatically resumed. Returns the updated session state.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
|
||||
| `sessionId` | string | Yes | The session ID to send the message to |
|
||||
| `message` | string | Yes | The message to send to Devin |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sessionId` | string | Unique identifier for the session |
|
||||
| `url` | string | URL to view the session in the Devin UI |
|
||||
| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) |
|
||||
| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) |
|
||||
| `title` | string | Session title |
|
||||
| `createdAt` | number | Unix timestamp when the session was created |
|
||||
| `updatedAt` | number | Unix timestamp when the session was last updated |
|
||||
| `acusConsumed` | number | ACUs consumed by the session |
|
||||
| `tags` | json | Tags associated with the session |
|
||||
| `pullRequests` | json | Pull requests created during the session |
|
||||
| `structuredOutput` | json | Structured output from the session |
|
||||
| `playbookId` | string | Associated playbook ID |
|
||||
|
||||
|
||||
774
apps/docs/content/docs/en/tools/gong.mdx
Normal file
774
apps/docs/content/docs/en/tools/gong.mdx
Normal file
@@ -0,0 +1,774 @@
|
||||
---
|
||||
title: Gong
|
||||
description: Revenue intelligence and conversation analytics
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="gong"
|
||||
color="#8039DF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Gong](https://www.gong.io/) is a revenue intelligence platform that captures and analyzes customer interactions across calls, emails, and meetings. By integrating Gong with Sim, your agents can access conversation data, user analytics, coaching metrics, and more through automated workflows.
|
||||
|
||||
The Gong integration in Sim provides tools to:
|
||||
|
||||
- **List and retrieve calls:** Fetch calls by date range, get individual call details, or retrieve extensive call data including trackers, topics, interaction stats, and points of interest.
|
||||
- **Access call transcripts:** Retrieve full transcripts with speaker turns, topics, and sentence-level timestamps for any recorded call.
|
||||
- **Manage users:** List all Gong users in your account or retrieve detailed information for a specific user, including settings, spoken languages, and contact details.
|
||||
- **Analyze activity and performance:** Pull aggregated activity statistics, interaction stats (longest monologue, interactivity, patience, question rate), and answered scorecard data for your team.
|
||||
- **Work with scorecards and trackers:** List scorecard definitions and keyword tracker configurations to understand how your team's conversations are being evaluated and monitored.
|
||||
- **Browse the call library:** List library folders and retrieve their contents, including call snippets and notes curated by your team.
|
||||
- **Access coaching metrics:** Retrieve coaching data for managers and their direct reports to track team development.
|
||||
- **List Engage flows:** Fetch sales engagement sequences (flows) with visibility and ownership details.
|
||||
- **Look up contacts by email or phone:** Find all Gong references to a specific email address or phone number, including related calls, emails, meetings, CRM data, and customer engagement events.
|
||||
|
||||
By combining these capabilities, you can automate sales coaching workflows, extract conversation insights, monitor team performance, sync Gong data with other systems, and build intelligent pipelines around your organization's revenue conversations -- all securely using your Gong API credentials.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Gong into your workflow. Access call recordings, transcripts, user data, activity stats, scorecards, trackers, library content, coaching metrics, and more via the Gong API.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `gong_list_calls`
|
||||
|
||||
Retrieve call data by date range from Gong.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `fromDateTime` | string | Yes | Start date/time in ISO-8601 format \(e.g., 2024-01-01T00:00:00Z\) |
|
||||
| `toDateTime` | string | No | End date/time in ISO-8601 format \(e.g., 2024-01-31T23:59:59Z\). If omitted, lists calls up to the most recent. |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
| `workspaceId` | string | No | Gong workspace ID to filter calls |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `calls` | array | List of calls matching the date range |
|
||||
| ↳ `id` | string | Gong's unique numeric identifier for the call |
|
||||
| ↳ `title` | string | Call title |
|
||||
| ↳ `scheduled` | string | Scheduled call time in ISO-8601 format |
|
||||
| ↳ `started` | string | Recording start time in ISO-8601 format |
|
||||
| ↳ `duration` | number | Call duration in seconds |
|
||||
| ↳ `direction` | string | Call direction \(Inbound/Outbound\) |
|
||||
| ↳ `system` | string | Communication platform used \(e.g., Outreach\) |
|
||||
| ↳ `scope` | string | Call scope: 'Internal', 'External', or 'Unknown' |
|
||||
| ↳ `media` | string | Media type \(e.g., Video\) |
|
||||
| ↳ `language` | string | Language code in ISO-639-2B format |
|
||||
| ↳ `url` | string | URL to the call in the Gong web app |
|
||||
| ↳ `primaryUserId` | string | Host team member identifier |
|
||||
| ↳ `workspaceId` | string | Workspace identifier |
|
||||
| ↳ `sdrDisposition` | string | SDR disposition classification |
|
||||
| ↳ `clientUniqueId` | string | Call identifier from the origin recording system |
|
||||
| ↳ `customData` | string | Metadata provided during call creation |
|
||||
| ↳ `purpose` | string | Call purpose |
|
||||
| ↳ `meetingUrl` | string | Web conference provider URL |
|
||||
| ↳ `isPrivate` | boolean | Whether the call is private |
|
||||
| ↳ `calendarEventId` | string | Calendar event identifier |
|
||||
| `cursor` | string | Pagination cursor for the next page |
|
||||
| `totalRecords` | number | Total number of records matching the filter |
|
||||
|
||||
### `gong_get_call`
|
||||
|
||||
Retrieve detailed data for a specific call from Gong.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `callId` | string | Yes | The Gong call ID to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Gong's unique numeric identifier for the call |
|
||||
| `title` | string | Call title |
|
||||
| `url` | string | URL to the call in the Gong web app |
|
||||
| `scheduled` | string | Scheduled call time in ISO-8601 format |
|
||||
| `started` | string | Recording start time in ISO-8601 format |
|
||||
| `duration` | number | Call duration in seconds |
|
||||
| `direction` | string | Call direction \(Inbound/Outbound\) |
|
||||
| `system` | string | Communication platform used \(e.g., Outreach\) |
|
||||
| `scope` | string | Call scope: 'Internal', 'External', or 'Unknown' |
|
||||
| `media` | string | Media type \(e.g., Video\) |
|
||||
| `language` | string | Language code in ISO-639-2B format |
|
||||
| `primaryUserId` | string | Host team member identifier |
|
||||
| `workspaceId` | string | Workspace identifier |
|
||||
| `sdrDisposition` | string | SDR disposition classification |
|
||||
| `clientUniqueId` | string | Call identifier from the origin recording system |
|
||||
| `customData` | string | Metadata provided during call creation |
|
||||
| `purpose` | string | Call purpose |
|
||||
| `meetingUrl` | string | Web conference provider URL |
|
||||
| `isPrivate` | boolean | Whether the call is private |
|
||||
| `calendarEventId` | string | Calendar event identifier |
|
||||
|
||||
### `gong_get_call_transcript`
|
||||
|
||||
Retrieve transcripts of calls from Gong by call IDs or date range.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `callIds` | string | No | Comma-separated list of call IDs to retrieve transcripts for |
|
||||
| `fromDateTime` | string | No | Start date/time filter in ISO-8601 format |
|
||||
| `toDateTime` | string | No | End date/time filter in ISO-8601 format |
|
||||
| `workspaceId` | string | No | Gong workspace ID to filter calls |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `callTranscripts` | array | List of call transcripts with speaker turns and sentences |
|
||||
| ↳ `callId` | string | Gong's unique numeric identifier for the call |
|
||||
| ↳ `transcript` | array | List of monologues in the call |
|
||||
| ↳ `speakerId` | string | Unique ID of the speaker, cross-reference with parties |
|
||||
| ↳ `topic` | string | Name of the topic being discussed |
|
||||
| ↳ `sentences` | array | List of sentences spoken in the monologue |
|
||||
| ↳ `start` | number | Start time of the sentence in milliseconds from call start |
|
||||
| ↳ `end` | number | End time of the sentence in milliseconds from call start |
|
||||
| ↳ `text` | string | The sentence text |
|
||||
| `cursor` | string | Pagination cursor for the next page |
|
||||
|
||||
### `gong_get_extensive_calls`
|
||||
|
||||
Retrieve detailed call data including trackers, topics, and highlights from Gong.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `callIds` | string | No | Comma-separated list of call IDs to retrieve detailed data for |
|
||||
| `fromDateTime` | string | No | Start date/time filter in ISO-8601 format |
|
||||
| `toDateTime` | string | No | End date/time filter in ISO-8601 format |
|
||||
| `workspaceId` | string | No | Gong workspace ID to filter calls |
|
||||
| `primaryUserIds` | string | No | Comma-separated list of user IDs to filter calls by host |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `calls` | array | List of detailed call objects with metadata, content, interaction stats, and collaboration data |
|
||||
| ↳ `metaData` | object | Call metadata \(same fields as CallBasicData\) |
|
||||
| ↳ `id` | string | Call ID |
|
||||
| ↳ `title` | string | Call title |
|
||||
| ↳ `scheduled` | string | Scheduled time in ISO-8601 |
|
||||
| ↳ `started` | string | Start time in ISO-8601 |
|
||||
| ↳ `duration` | number | Duration in seconds |
|
||||
| ↳ `direction` | string | Call direction |
|
||||
| ↳ `system` | string | Communication platform |
|
||||
| ↳ `scope` | string | Internal/External/Unknown |
|
||||
| ↳ `media` | string | Media type |
|
||||
| ↳ `language` | string | Language code \(ISO-639-2B\) |
|
||||
| ↳ `url` | string | Gong web app URL |
|
||||
| ↳ `primaryUserId` | string | Host user ID |
|
||||
| ↳ `workspaceId` | string | Workspace ID |
|
||||
| ↳ `sdrDisposition` | string | SDR disposition |
|
||||
| ↳ `clientUniqueId` | string | Origin system call ID |
|
||||
| ↳ `customData` | string | Custom metadata |
|
||||
| ↳ `purpose` | string | Call purpose |
|
||||
| ↳ `meetingUrl` | string | Meeting URL |
|
||||
| ↳ `isPrivate` | boolean | Whether call is private |
|
||||
| ↳ `calendarEventId` | string | Calendar event ID |
|
||||
| ↳ `context` | array | Links to external systems \(CRM, Dialer, etc.\) |
|
||||
| ↳ `system` | string | External system name \(e.g., Salesforce\) |
|
||||
| ↳ `objects` | array | List of objects within the external system |
|
||||
| ↳ `parties` | array | List of call participants |
|
||||
| ↳ `id` | string | Unique participant ID in the call |
|
||||
| ↳ `name` | string | Participant name |
|
||||
| ↳ `emailAddress` | string | Email address |
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `phoneNumber` | string | Phone number |
|
||||
| ↳ `speakerId` | string | Speaker ID for transcript cross-reference |
|
||||
| ↳ `userId` | string | Gong user ID |
|
||||
| ↳ `affiliation` | string | Company or non-company |
|
||||
| ↳ `methods` | array | Whether invited or attended |
|
||||
| ↳ `context` | array | Links to external systems for this party |
|
||||
| ↳ `content` | object | Call content data |
|
||||
| ↳ `structure` | array | Call agenda parts |
|
||||
| ↳ `name` | string | Agenda name |
|
||||
| ↳ `duration` | number | Duration of this part in seconds |
|
||||
| ↳ `topics` | array | Topics and their durations |
|
||||
| ↳ `name` | string | Topic name \(e.g., Pricing\) |
|
||||
| ↳ `duration` | number | Time spent on topic in seconds |
|
||||
| ↳ `trackers` | array | Trackers found in the call |
|
||||
| ↳ `id` | string | Tracker ID |
|
||||
| ↳ `name` | string | Tracker name |
|
||||
| ↳ `count` | number | Number of occurrences |
|
||||
| ↳ `type` | string | Keyword or Smart |
|
||||
| ↳ `occurrences` | array | Details for each occurrence |
|
||||
| ↳ `speakerId` | string | Speaker who said it |
|
||||
| ↳ `startTime` | number | Seconds from call start |
|
||||
| ↳ `phrases` | array | Per-phrase occurrence counts |
|
||||
| ↳ `phrase` | string | Specific phrase |
|
||||
| ↳ `count` | number | Occurrences of this phrase |
|
||||
| ↳ `occurrences` | array | Details per occurrence |
|
||||
| ↳ `highlights` | array | AI-generated highlights including next steps, action items, and key moments |
|
||||
| ↳ `title` | string | Title of the highlight |
|
||||
| ↳ `interaction` | object | Interaction statistics |
|
||||
| ↳ `interactionStats` | array | Interaction stats per user |
|
||||
| ↳ `userId` | string | Gong user ID |
|
||||
| ↳ `userEmailAddress` | string | User email |
|
||||
| ↳ `personInteractionStats` | array | Stats list \(Longest Monologue, Interactivity, Patience, etc.\) |
|
||||
| ↳ `name` | string | Stat name |
|
||||
| ↳ `value` | number | Stat value |
|
||||
| ↳ `speakers` | array | Talk duration per speaker |
|
||||
| ↳ `id` | string | Participant ID |
|
||||
| ↳ `userId` | string | Gong user ID |
|
||||
| ↳ `talkTime` | number | Talk duration in seconds |
|
||||
| ↳ `video` | array | Video statistics |
|
||||
| ↳ `name` | string | Segment type: Browser, Presentation, WebcamPrimaryUser, WebcamNonCompany, Webcam |
|
||||
| ↳ `duration` | number | Total segment duration in seconds |
|
||||
| ↳ `questions` | object | Question counts |
|
||||
| ↳ `companyCount` | number | Questions by company speakers |
|
||||
| ↳ `nonCompanyCount` | number | Questions by non-company speakers |
|
||||
| ↳ `collaboration` | object | Collaboration data |
|
||||
| ↳ `publicComments` | array | Public comments on the call |
|
||||
| ↳ `id` | string | Comment ID |
|
||||
| ↳ `commenterUserId` | string | Commenter user ID |
|
||||
| ↳ `comment` | string | Comment text |
|
||||
| ↳ `posted` | string | Posted time in ISO-8601 |
|
||||
| ↳ `audioStartTime` | number | Seconds from call start the comment refers to |
|
||||
| ↳ `audioEndTime` | number | Seconds from call start the comment end refers to |
|
||||
| ↳ `duringCall` | boolean | Whether the comment was posted during the call |
|
||||
| ↳ `inReplyTo` | string | ID of original comment if this is a reply |
|
||||
| ↳ `media` | object | Media download URLs \(available for 8 hours\) |
|
||||
| ↳ `audioUrl` | string | Audio download URL |
|
||||
| ↳ `videoUrl` | string | Video download URL |
|
||||
| `cursor` | string | Pagination cursor for the next page |
|
||||
|
||||
### `gong_list_users`
|
||||
|
||||
List all users in your Gong account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
| `includeAvatars` | string | No | Whether to include avatar URLs \(true/false\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | List of Gong users |
|
||||
| ↳ `id` | string | Unique numeric user ID \(up to 20 digits\) |
|
||||
| ↳ `emailAddress` | string | User email address |
|
||||
| ↳ `created` | string | User creation timestamp \(ISO-8601\) |
|
||||
| ↳ `active` | boolean | Whether the user is active |
|
||||
| ↳ `emailAliases` | array | Alternative email addresses for the user |
|
||||
| ↳ `trustedEmailAddress` | string | Trusted email address for the user |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `phoneNumber` | string | Phone number |
|
||||
| ↳ `extension` | string | Phone extension number |
|
||||
| ↳ `personalMeetingUrls` | array | Personal meeting URLs |
|
||||
| ↳ `settings` | object | User settings |
|
||||
| ↳ `webConferencesRecorded` | boolean | Whether web conferences are recorded |
|
||||
| ↳ `preventWebConferenceRecording` | boolean | Whether web conference recording is prevented |
|
||||
| ↳ `telephonyCallsImported` | boolean | Whether telephony calls are imported |
|
||||
| ↳ `emailsImported` | boolean | Whether emails are imported |
|
||||
| ↳ `preventEmailImport` | boolean | Whether email import is prevented |
|
||||
| ↳ `nonRecordedMeetingsImported` | boolean | Whether non-recorded meetings are imported |
|
||||
| ↳ `gongConnectEnabled` | boolean | Whether Gong Connect is enabled |
|
||||
| ↳ `managerId` | string | Manager user ID |
|
||||
| ↳ `meetingConsentPageUrl` | string | Meeting consent page URL |
|
||||
| ↳ `spokenLanguages` | array | Languages spoken by the user |
|
||||
| ↳ `language` | string | Language code |
|
||||
| ↳ `primary` | boolean | Whether this is the primary language |
|
||||
| `cursor` | string | Pagination cursor for the next page |
|
||||
| `totalRecords` | number | Total number of user records |
|
||||
| `currentPageSize` | number | Number of records in the current page |
|
||||
| `currentPageNumber` | number | Current page number |
|
||||
|
||||
### `gong_get_user`
|
||||
|
||||
Retrieve details for a specific user from Gong.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `userId` | string | Yes | The Gong user ID to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Unique numeric user ID \(up to 20 digits\) |
|
||||
| `emailAddress` | string | User email address |
|
||||
| `created` | string | User creation timestamp \(ISO-8601\) |
|
||||
| `active` | boolean | Whether the user is active |
|
||||
| `emailAliases` | array | Alternative email addresses for the user |
|
||||
| `trustedEmailAddress` | string | Trusted email address for the user |
|
||||
| `firstName` | string | First name |
|
||||
| `lastName` | string | Last name |
|
||||
| `title` | string | Job title |
|
||||
| `phoneNumber` | string | Phone number |
|
||||
| `extension` | string | Phone extension number |
|
||||
| `personalMeetingUrls` | array | Personal meeting URLs |
|
||||
| `settings` | object | User settings |
|
||||
| ↳ `webConferencesRecorded` | boolean | Whether web conferences are recorded |
|
||||
| ↳ `preventWebConferenceRecording` | boolean | Whether web conference recording is prevented |
|
||||
| ↳ `telephonyCallsImported` | boolean | Whether telephony calls are imported |
|
||||
| ↳ `emailsImported` | boolean | Whether emails are imported |
|
||||
| ↳ `preventEmailImport` | boolean | Whether email import is prevented |
|
||||
| ↳ `nonRecordedMeetingsImported` | boolean | Whether non-recorded meetings are imported |
|
||||
| ↳ `gongConnectEnabled` | boolean | Whether Gong Connect is enabled |
|
||||
| `managerId` | string | Manager user ID |
|
||||
| `meetingConsentPageUrl` | string | Meeting consent page URL |
|
||||
| `spokenLanguages` | array | Languages spoken by the user |
|
||||
| ↳ `language` | string | Language code |
|
||||
| ↳ `primary` | boolean | Whether this is the primary language |
|
||||
|
||||
### `gong_aggregate_activity`
|
||||
|
||||
Retrieve aggregated activity statistics for users by date range from Gong.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `userIds` | string | No | Comma-separated list of Gong user IDs \(up to 20 digits each\) |
|
||||
| `fromDate` | string | Yes | Start date in YYYY-MM-DD format \(inclusive, in company timezone\) |
|
||||
| `toDate` | string | Yes | End date in YYYY-MM-DD format \(exclusive, in company timezone, cannot exceed current day\) |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `usersActivity` | array | Aggregated activity statistics per user |
|
||||
| ↳ `userId` | string | Gong's unique numeric identifier for the user |
|
||||
| ↳ `userEmailAddress` | string | Email address of the Gong user |
|
||||
| ↳ `callsAsHost` | number | Number of recorded calls this user hosted |
|
||||
| ↳ `callsAttended` | number | Number of calls where this user was a participant \(not host\) |
|
||||
| ↳ `callsGaveFeedback` | number | Number of recorded calls the user gave feedback on |
|
||||
| ↳ `callsReceivedFeedback` | number | Number of recorded calls the user received feedback on |
|
||||
| ↳ `callsRequestedFeedback` | number | Number of recorded calls the user requested feedback on |
|
||||
| ↳ `callsScorecardsFilled` | number | Number of scorecards the user completed |
|
||||
| ↳ `callsScorecardsReceived` | number | Number of calls where someone filled a scorecard on the user's calls |
|
||||
| ↳ `ownCallsListenedTo` | number | Number of the user's own calls the user listened to |
|
||||
| ↳ `othersCallsListenedTo` | number | Number of other users' calls the user listened to |
|
||||
| ↳ `callsSharedInternally` | number | Number of calls the user shared internally |
|
||||
| ↳ `callsSharedExternally` | number | Number of calls the user shared externally |
|
||||
| ↳ `callsCommentsGiven` | number | Number of calls where the user provided at least one comment |
|
||||
| ↳ `callsCommentsReceived` | number | Number of calls where the user received at least one comment |
|
||||
| ↳ `callsMarkedAsFeedbackGiven` | number | Number of calls where the user selected Mark as reviewed |
|
||||
| ↳ `callsMarkedAsFeedbackReceived` | number | Number of calls where others selected Mark as reviewed on the user's calls |
|
||||
| `timeZone` | string | The company's defined timezone in Gong |
|
||||
| `fromDateTime` | string | Start of results in ISO-8601 format |
|
||||
| `toDateTime` | string | End of results in ISO-8601 format |
|
||||
| `cursor` | string | Pagination cursor for the next page |
|
||||
|
||||
### `gong_interaction_stats`
|
||||
|
||||
Retrieve interaction statistics for users by date range from Gong. Only includes calls with Whisper enabled.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `userIds` | string | No | Comma-separated list of Gong user IDs \(up to 20 digits each\) |
|
||||
| `fromDate` | string | Yes | Start date in YYYY-MM-DD format \(inclusive, in company timezone\) |
|
||||
| `toDate` | string | Yes | End date in YYYY-MM-DD format \(exclusive, in company timezone, cannot exceed current day\) |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `peopleInteractionStats` | array | Email address of the Gong user |
|
||||
| ↳ `userId` | string | Gong's unique numeric identifier for the user |
|
||||
| ↳ `userEmailAddress` | string | Email address of the Gong user |
|
||||
| ↳ `personInteractionStats` | array | List of interaction stat measurements for this user |
|
||||
| ↳ `name` | string | Stat name \(e.g. Longest Monologue, Interactivity, Patience, Question Rate\) |
|
||||
| ↳ `value` | number | Stat measurement value \(can be double or integer\) |
|
||||
| `timeZone` | string | The company's defined timezone in Gong |
|
||||
| `fromDateTime` | string | Start of results in ISO-8601 format |
|
||||
| `toDateTime` | string | End of results in ISO-8601 format |
|
||||
| `cursor` | string | Pagination cursor for the next page |
|
||||
|
||||
### `gong_answered_scorecards`
|
||||
|
||||
Retrieve answered scorecards for reviewed users or by date range from Gong.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `callFromDate` | string | No | Start date for calls in YYYY-MM-DD format \(inclusive, in company timezone\). Defaults to earliest recorded call. |
|
||||
| `callToDate` | string | No | End date for calls in YYYY-MM-DD format \(exclusive, in company timezone\). Defaults to latest recorded call. |
|
||||
| `reviewFromDate` | string | No | Start date for reviews in YYYY-MM-DD format \(inclusive, in company timezone\). Defaults to earliest reviewed call. |
|
||||
| `reviewToDate` | string | No | End date for reviews in YYYY-MM-DD format \(exclusive, in company timezone\). Defaults to latest reviewed call. |
|
||||
| `scorecardIds` | string | No | Comma-separated list of scorecard IDs to filter by |
|
||||
| `reviewedUserIds` | string | No | Comma-separated list of reviewed user IDs to filter by |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `answeredScorecards` | array | List of answered scorecards with scores and answers |
|
||||
| ↳ `answeredScorecardId` | number | Identifier of the answered scorecard |
|
||||
| ↳ `scorecardId` | number | Identifier of the scorecard |
|
||||
| ↳ `scorecardName` | string | Scorecard name |
|
||||
| ↳ `callId` | number | Gong's unique numeric identifier for the call |
|
||||
| ↳ `callStartTime` | string | Date/time of the call in ISO-8601 format |
|
||||
| ↳ `reviewedUserId` | number | User ID of the team member being reviewed |
|
||||
| ↳ `reviewerUserId` | number | User ID of the team member who completed the scorecard |
|
||||
| ↳ `reviewTime` | string | Date/time when the review was completed in ISO-8601 format |
|
||||
| ↳ `visibilityType` | string | Visibility type of the scorecard answer |
|
||||
| ↳ `answers` | array | Answers in the answered scorecard |
|
||||
| ↳ `questionId` | number | Identifier of the question |
|
||||
| ↳ `questionRevisionId` | number | Identifier of the revision version of the question |
|
||||
| ↳ `isOverall` | boolean | Whether this is the overall question |
|
||||
| ↳ `score` | number | Score between 1 to 5 if answered, null otherwise |
|
||||
| ↳ `answerText` | string | The answer's text if answered, null otherwise |
|
||||
| ↳ `notApplicable` | boolean | Whether the question is not applicable to this call |
|
||||
| `cursor` | string | Pagination cursor for the next page |
|
||||
|
||||
### `gong_list_library_folders`
|
||||
|
||||
Retrieve library folders from Gong.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `workspaceId` | string | No | Gong workspace ID to filter folders |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `folders` | array | List of library folders with id, name, and parent relationships |
|
||||
| ↳ `id` | string | Gong unique numeric identifier for the folder |
|
||||
| ↳ `name` | string | Display name of the folder |
|
||||
| ↳ `parentFolderId` | string | Gong unique numeric identifier for the parent folder \(null for root folder\) |
|
||||
| ↳ `createdBy` | string | Gong unique numeric identifier for the user who added the folder |
|
||||
| ↳ `updated` | string | Folder's last update time in ISO-8601 format |
|
||||
|
||||
### `gong_get_folder_content`
|
||||
|
||||
Retrieve the list of calls in a specific library folder from Gong.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `folderId` | string | Yes | The library folder ID to retrieve content for |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `folderId` | string | Gong's unique numeric identifier for the folder |
|
||||
| `folderName` | string | Display name of the folder |
|
||||
| `createdBy` | string | Gong's unique numeric identifier for the user who added the folder |
|
||||
| `updated` | string | Folder's last update time in ISO-8601 format |
|
||||
| `calls` | array | List of calls in the library folder |
|
||||
| ↳ `id` | string | Gong unique numeric identifier of the call |
|
||||
| ↳ `title` | string | The title of the call |
|
||||
| ↳ `note` | string | A note attached to the call in the folder |
|
||||
| ↳ `addedBy` | string | Gong unique numeric identifier for the user who added the call |
|
||||
| ↳ `created` | string | Date and time the call was added to folder in ISO-8601 format |
|
||||
| ↳ `url` | string | URL of the call |
|
||||
| ↳ `snippet` | object | Call snippet time range |
|
||||
| ↳ `fromSec` | number | Snippet start in seconds relative to call start |
|
||||
| ↳ `toSec` | number | Snippet end in seconds relative to call start |
|
||||
|
||||
### `gong_list_scorecards`
|
||||
|
||||
Retrieve scorecard definitions from Gong settings.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `scorecards` | array | List of scorecard definitions with questions |
|
||||
| ↳ `scorecardId` | string | Unique identifier for the scorecard |
|
||||
| ↳ `scorecardName` | string | Display name of the scorecard |
|
||||
| ↳ `workspaceId` | string | Workspace identifier associated with this scorecard |
|
||||
| ↳ `enabled` | boolean | Whether the scorecard is active |
|
||||
| ↳ `updaterUserId` | string | ID of the user who last modified the scorecard |
|
||||
| ↳ `created` | string | Creation timestamp in ISO-8601 format |
|
||||
| ↳ `updated` | string | Last update timestamp in ISO-8601 format |
|
||||
| ↳ `questions` | array | List of questions in the scorecard |
|
||||
| ↳ `questionId` | string | Unique identifier for the question |
|
||||
| ↳ `questionText` | string | The text content of the question |
|
||||
| ↳ `questionRevisionId` | string | Identifier for the specific revision of the question |
|
||||
| ↳ `isOverall` | boolean | Whether this is the primary overall question |
|
||||
| ↳ `created` | string | Question creation timestamp in ISO-8601 format |
|
||||
| ↳ `updated` | string | Question last update timestamp in ISO-8601 format |
|
||||
| ↳ `updaterUserId` | string | ID of the user who last modified the question |
|
||||
|
||||
### `gong_list_trackers`
|
||||
|
||||
Retrieve smart tracker and keyword tracker definitions from Gong settings.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `workspaceId` | string | No | The ID of the workspace the keyword trackers are in. When empty, all trackers in all workspaces are returned. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `trackers` | array | List of keyword tracker definitions |
|
||||
| ↳ `trackerId` | string | Unique identifier for the tracker |
|
||||
| ↳ `trackerName` | string | Display name of the tracker |
|
||||
| ↳ `workspaceId` | string | ID of the workspace containing the tracker |
|
||||
| ↳ `languageKeywords` | array | Keywords organized by language |
|
||||
| ↳ `language` | string | ISO 639-2/B language code \("mul" means keywords apply across all languages\) |
|
||||
| ↳ `keywords` | array | Words and phrases in the designated language |
|
||||
| ↳ `includeRelatedForms` | boolean | Whether to include different word forms |
|
||||
| ↳ `affiliation` | string | Speaker affiliation filter: "Anyone", "Company", or "NonCompany" |
|
||||
| ↳ `partOfQuestion` | boolean | Whether to track keywords only within questions |
|
||||
| ↳ `saidAt` | string | Position in call: "Anytime", "First", or "Last" |
|
||||
| ↳ `saidAtInterval` | number | Duration to search \(in minutes or percentage\) |
|
||||
| ↳ `saidAtUnit` | string | Unit for saidAtInterval |
|
||||
| ↳ `saidInTopics` | array | Topics where keywords should be detected |
|
||||
| ↳ `saidInCallParts` | array | Specific call segments to monitor |
|
||||
| ↳ `filterQuery` | string | JSON-formatted call filtering criteria |
|
||||
| ↳ `created` | string | Creation timestamp in ISO-8601 format |
|
||||
| ↳ `creatorUserId` | string | ID of the user who created the tracker \(null for built-in trackers\) |
|
||||
| ↳ `updated` | string | Last modification timestamp in ISO-8601 format |
|
||||
| ↳ `updaterUserId` | string | ID of the user who last modified the tracker |
|
||||
|
||||
### `gong_list_workspaces`
|
||||
|
||||
List all company workspaces in Gong.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `workspaces` | array | List of Gong workspaces |
|
||||
| ↳ `id` | string | Gong unique numeric identifier for the workspace |
|
||||
| ↳ `name` | string | Display name of the workspace |
|
||||
| ↳ `description` | string | Description of the workspace's purpose or content |
|
||||
|
||||
### `gong_list_flows`
|
||||
|
||||
List Gong Engage flows (sales engagement sequences).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `flowOwnerEmail` | string | Yes | Email of a Gong user. The API will return 'PERSONAL' flows belonging to this user in addition to 'COMPANY' flows. |
|
||||
| `workspaceId` | string | No | Optional workspace ID to filter flows to a specific workspace |
|
||||
| `cursor` | string | No | Pagination cursor from a previous API call to retrieve the next page of records |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `requestId` | string | A Gong request reference ID for troubleshooting purposes |
|
||||
| `flows` | array | List of Gong Engage flows |
|
||||
| ↳ `id` | string | The ID of the flow |
|
||||
| ↳ `name` | string | The name of the flow |
|
||||
| ↳ `folderId` | string | The ID of the folder this flow is under |
|
||||
| ↳ `folderName` | string | The name of the folder this flow is under |
|
||||
| ↳ `visibility` | string | The flow visibility type \(COMPANY, PERSONAL, or SHARED\) |
|
||||
| ↳ `creationDate` | string | Creation time of the flow in ISO-8601 format |
|
||||
| ↳ `exclusive` | boolean | Indicates whether a prospect in this flow can be added to other flows |
|
||||
| `totalRecords` | number | Total number of flow records available |
|
||||
| `currentPageSize` | number | Number of records returned in the current page |
|
||||
| `currentPageNumber` | number | Current page number |
|
||||
| `cursor` | string | Pagination cursor for retrieving the next page of records |
|
||||
|
||||
### `gong_get_coaching`
|
||||
|
||||
Retrieve coaching metrics for a manager from Gong.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `managerId` | string | Yes | Gong user ID of the manager |
|
||||
| `workspaceId` | string | Yes | Gong workspace ID |
|
||||
| `fromDate` | string | Yes | Start date in ISO-8601 format |
|
||||
| `toDate` | string | Yes | End date in ISO-8601 format |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `requestId` | string | A Gong request reference ID for troubleshooting purposes |
|
||||
| `coachingData` | array | The manager user information |
|
||||
| ↳ `manager` | object | The manager user information |
|
||||
| ↳ `id` | string | Gong unique numeric identifier for the user |
|
||||
| ↳ `emailAddress` | string | Email address of the Gong user |
|
||||
| ↳ `firstName` | string | First name of the Gong user |
|
||||
| ↳ `lastName` | string | Last name of the Gong user |
|
||||
| ↳ `title` | string | Job title of the Gong user |
|
||||
| ↳ `directReportsMetrics` | array | Coaching metrics for each direct report |
|
||||
| ↳ `report` | object | The direct report user information |
|
||||
| ↳ `id` | string | Gong unique numeric identifier for the user |
|
||||
| ↳ `emailAddress` | string | Email address of the Gong user |
|
||||
| ↳ `firstName` | string | First name of the Gong user |
|
||||
| ↳ `lastName` | string | Last name of the Gong user |
|
||||
| ↳ `title` | string | Job title of the Gong user |
|
||||
| ↳ `metrics` | json | A map of metric names to arrays of string values representing coaching metrics |
|
||||
|
||||
### `gong_lookup_email`
|
||||
|
||||
Find all references to an email address in Gong (calls, email messages, meetings, CRM data, engagement).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `emailAddress` | string | Yes | Email address to look up |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `requestId` | string | Gong request reference ID for troubleshooting |
|
||||
| `calls` | array | Related calls referencing this email address |
|
||||
| ↳ `id` | string | Gong's unique numeric identifier for the call \(up to 20 digits\) |
|
||||
| ↳ `status` | string | Call status |
|
||||
| ↳ `externalSystems` | array | Links to external systems such as CRM, Telephony System, etc. |
|
||||
| ↳ `system` | string | External system name |
|
||||
| ↳ `objects` | array | List of objects within the external system |
|
||||
| ↳ `objectType` | string | Object type |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| `emails` | array | Related email messages referencing this email address |
|
||||
| ↳ `id` | string | Gong's unique 32 character identifier for the email message |
|
||||
| ↳ `from` | string | The sender's email address |
|
||||
| ↳ `sentTime` | string | Date and time the email was sent in ISO-8601 format |
|
||||
| ↳ `mailbox` | string | The mailbox from which the email was retrieved |
|
||||
| ↳ `messageHash` | string | Hash code of the email message |
|
||||
| `meetings` | array | Related meetings referencing this email address |
|
||||
| ↳ `id` | string | Gong's unique identifier for the meeting |
|
||||
| `customerData` | array | Links to data from external systems \(CRM, Telephony, etc.\) that reference this email |
|
||||
| ↳ `system` | string | External system name |
|
||||
| ↳ `objects` | array | List of objects in the external system |
|
||||
| ↳ `id` | string | Gong's unique numeric identifier for the Lead or Contact \(up to 20 digits\) |
|
||||
| ↳ `objectType` | string | Object type |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `mirrorId` | string | CRM Mirror ID |
|
||||
| ↳ `fields` | array | Object fields |
|
||||
| ↳ `name` | string | Field name |
|
||||
| ↳ `value` | json | Field value |
|
||||
| `customerEngagement` | array | Customer engagement events \(such as viewing external shared calls\) |
|
||||
| ↳ `eventType` | string | Event type |
|
||||
| ↳ `eventName` | string | Event name |
|
||||
| ↳ `timestamp` | string | Date and time the event occurred in ISO-8601 format |
|
||||
| ↳ `contentId` | string | Event content ID |
|
||||
| ↳ `contentUrl` | string | Event content URL |
|
||||
| ↳ `reportingSystem` | string | Event reporting system |
|
||||
| ↳ `sourceEventId` | string | Source event ID |
|
||||
|
||||
### `gong_lookup_phone`
|
||||
|
||||
Find all references to a phone number in Gong (calls, email messages, meetings, CRM data, and associated contacts).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKey` | string | Yes | Gong API Access Key |
|
||||
| `accessKeySecret` | string | Yes | Gong API Access Key Secret |
|
||||
| `phoneNumber` | string | Yes | Phone number to look up \(must start with + followed by country code\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `requestId` | string | Gong request reference ID for troubleshooting |
|
||||
| `suppliedPhoneNumber` | string | The phone number that was supplied in the request |
|
||||
| `matchingPhoneNumbers` | array | Phone numbers found in the system that match the supplied number |
|
||||
| `emailAddresses` | array | Email addresses associated with the phone number |
|
||||
| `calls` | array | Related calls referencing this phone number |
|
||||
| ↳ `id` | string | Gong's unique numeric identifier for the call \(up to 20 digits\) |
|
||||
| ↳ `status` | string | Call status |
|
||||
| ↳ `externalSystems` | array | Links to external systems such as CRM, Telephony System, etc. |
|
||||
| ↳ `system` | string | External system name |
|
||||
| ↳ `objects` | array | List of objects within the external system |
|
||||
| ↳ `objectType` | string | Object type |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| `emails` | array | Related email messages associated with contacts matching this phone number |
|
||||
| ↳ `id` | string | Gong's unique 32 character identifier for the email message |
|
||||
| ↳ `from` | string | The sender's email address |
|
||||
| ↳ `sentTime` | string | Date and time the email was sent in ISO-8601 format |
|
||||
| ↳ `mailbox` | string | The mailbox from which the email was retrieved |
|
||||
| ↳ `messageHash` | string | Hash code of the email message |
|
||||
| `meetings` | array | Related meetings associated with this phone number |
|
||||
| ↳ `id` | string | Gong's unique identifier for the meeting |
|
||||
| `customerData` | array | Links to data from external systems \(CRM, Telephony, etc.\) that reference this phone number |
|
||||
| ↳ `system` | string | External system name |
|
||||
| ↳ `objects` | array | List of objects in the external system |
|
||||
| ↳ `id` | string | Gong's unique numeric identifier for the Lead or Contact \(up to 20 digits\) |
|
||||
| ↳ `objectType` | string | Object type |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `mirrorId` | string | CRM Mirror ID |
|
||||
| ↳ `fields` | array | Object fields |
|
||||
| ↳ `name` | string | Field name |
|
||||
| ↳ `value` | json | Field value |
|
||||
|
||||
|
||||
168
apps/docs/content/docs/en/tools/google_bigquery.mdx
Normal file
168
apps/docs/content/docs/en/tools/google_bigquery.mdx
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
title: Google BigQuery
|
||||
description: Query, list, and insert data in Google BigQuery
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_bigquery"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google BigQuery](https://cloud.google.com/bigquery) is Google Cloud's fully managed, serverless data warehouse designed for large-scale data analytics. BigQuery lets you run fast SQL queries on massive datasets, making it ideal for business intelligence, data exploration, and machine learning pipelines. It supports standard SQL, streaming inserts, and integrates with the broader Google Cloud ecosystem.
|
||||
|
||||
In Sim, the Google BigQuery integration allows your agents to query datasets, list tables, inspect schemas, and insert rows as part of automated workflows. This enables use cases such as automated reporting, data pipeline orchestration, real-time data ingestion, and analytics-driven decision making. By connecting Sim with BigQuery, your agents can pull insights from petabytes of data, write results back to tables, and keep your analytics workflows running without manual intervention.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect to Google BigQuery to run SQL queries, list datasets and tables, get table metadata, and insert rows.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `google_bigquery_query`
|
||||
|
||||
Run a SQL query against Google BigQuery and return the results
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Google Cloud project ID |
|
||||
| `query` | string | Yes | SQL query to execute |
|
||||
| `useLegacySql` | boolean | No | Whether to use legacy SQL syntax \(default: false\) |
|
||||
| `maxResults` | number | No | Maximum number of rows to return |
|
||||
| `defaultDatasetId` | string | No | Default dataset for unqualified table names |
|
||||
| `location` | string | No | Processing location \(e.g., "US", "EU"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `columns` | array | Array of column names from the query result |
|
||||
| `rows` | array | Array of row objects keyed by column name |
|
||||
| `totalRows` | string | Total number of rows in the complete result set |
|
||||
| `jobComplete` | boolean | Whether the query completed within the timeout |
|
||||
| `totalBytesProcessed` | string | Total bytes processed by the query |
|
||||
| `cacheHit` | boolean | Whether the query result was served from cache |
|
||||
| `jobReference` | object | Job reference \(useful when jobComplete is false\) |
|
||||
| ↳ `projectId` | string | Project ID containing the job |
|
||||
| ↳ `jobId` | string | Unique job identifier |
|
||||
| ↳ `location` | string | Geographic location of the job |
|
||||
| `pageToken` | string | Token for fetching additional result pages |
|
||||
|
||||
### `google_bigquery_list_datasets`
|
||||
|
||||
List all datasets in a Google BigQuery project
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Google Cloud project ID |
|
||||
| `maxResults` | number | No | Maximum number of datasets to return |
|
||||
| `pageToken` | string | No | Token for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `datasets` | array | Array of dataset objects |
|
||||
| ↳ `datasetId` | string | Unique dataset identifier |
|
||||
| ↳ `projectId` | string | Project ID containing this dataset |
|
||||
| ↳ `friendlyName` | string | Descriptive name for the dataset |
|
||||
| ↳ `location` | string | Geographic location where the data resides |
|
||||
| `nextPageToken` | string | Token for fetching next page of results |
|
||||
|
||||
### `google_bigquery_list_tables`
|
||||
|
||||
List all tables in a Google BigQuery dataset
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Google Cloud project ID |
|
||||
| `datasetId` | string | Yes | BigQuery dataset ID |
|
||||
| `maxResults` | number | No | Maximum number of tables to return |
|
||||
| `pageToken` | string | No | Token for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tables` | array | Array of table objects |
|
||||
| ↳ `tableId` | string | Table identifier |
|
||||
| ↳ `datasetId` | string | Dataset ID containing this table |
|
||||
| ↳ `projectId` | string | Project ID containing this table |
|
||||
| ↳ `type` | string | Table type \(TABLE, VIEW, EXTERNAL, etc.\) |
|
||||
| ↳ `friendlyName` | string | User-friendly name for the table |
|
||||
| ↳ `creationTime` | string | Time when created, in milliseconds since epoch |
|
||||
| `totalItems` | number | Total number of tables in the dataset |
|
||||
| `nextPageToken` | string | Token for fetching next page of results |
|
||||
|
||||
### `google_bigquery_get_table`
|
||||
|
||||
Get metadata and schema for a Google BigQuery table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Google Cloud project ID |
|
||||
| `datasetId` | string | Yes | BigQuery dataset ID |
|
||||
| `tableId` | string | Yes | BigQuery table ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tableId` | string | Table ID |
|
||||
| `datasetId` | string | Dataset ID |
|
||||
| `projectId` | string | Project ID |
|
||||
| `type` | string | Table type \(TABLE, VIEW, SNAPSHOT, MATERIALIZED_VIEW, EXTERNAL\) |
|
||||
| `description` | string | Table description |
|
||||
| `numRows` | string | Total number of rows |
|
||||
| `numBytes` | string | Total size in bytes, excluding data in streaming buffer |
|
||||
| `schema` | array | Array of column definitions |
|
||||
| ↳ `name` | string | Column name |
|
||||
| ↳ `type` | string | Data type \(STRING, INTEGER, FLOAT, BOOLEAN, TIMESTAMP, RECORD, etc.\) |
|
||||
| ↳ `mode` | string | Column mode \(NULLABLE, REQUIRED, or REPEATED\) |
|
||||
| ↳ `description` | string | Column description |
|
||||
| `creationTime` | string | Table creation time \(milliseconds since epoch\) |
|
||||
| `lastModifiedTime` | string | Last modification time \(milliseconds since epoch\) |
|
||||
| `location` | string | Geographic location where the table resides |
|
||||
|
||||
### `google_bigquery_insert_rows`
|
||||
|
||||
Insert rows into a Google BigQuery table using streaming insert
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Google Cloud project ID |
|
||||
| `datasetId` | string | Yes | BigQuery dataset ID |
|
||||
| `tableId` | string | Yes | BigQuery table ID |
|
||||
| `rows` | string | Yes | JSON array of row objects to insert |
|
||||
| `skipInvalidRows` | boolean | No | Whether to insert valid rows even if some are invalid |
|
||||
| `ignoreUnknownValues` | boolean | No | Whether to ignore columns not in the table schema |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `insertedRows` | number | Number of rows successfully inserted |
|
||||
| `errors` | array | Array of per-row insertion errors \(empty if all succeeded\) |
|
||||
| ↳ `index` | number | Zero-based index of the row that failed |
|
||||
| ↳ `errors` | array | Error details for this row |
|
||||
| ↳ `reason` | string | Short error code summarizing the error |
|
||||
| ↳ `location` | string | Where the error occurred |
|
||||
| ↳ `message` | string | Human-readable error description |
|
||||
|
||||
|
||||
@@ -10,6 +10,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google Books](https://books.google.com) is Google's comprehensive book discovery and metadata service, providing access to millions of books from publishers, libraries, and digitized collections worldwide. The Google Books API enables programmatic search and retrieval of detailed book information including titles, authors, descriptions, ratings, and publication details.
|
||||
|
||||
In Sim, the Google Books integration allows your agents to search for books and retrieve volume details as part of automated workflows. This enables use cases such as content research, reading list curation, bibliographic data enrichment, ISBN lookups, and knowledge gathering from published works. By connecting Sim with Google Books, your agents can discover and analyze book metadata, filter by availability or format, and incorporate literary references into their outputs—all without manual research.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Search for books using the Google Books API. Find volumes by title, author, ISBN, or keywords, and retrieve detailed information about specific books including descriptions, ratings, and publication details.
|
||||
|
||||
205
apps/docs/content/docs/en/tools/google_tasks.mdx
Normal file
205
apps/docs/content/docs/en/tools/google_tasks.mdx
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: Google Tasks
|
||||
description: Manage Google Tasks
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_tasks"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google Tasks](https://support.google.com/tasks) is Google's lightweight task management service, integrated into Gmail, Google Calendar, and the standalone Google Tasks app. It provides a simple way to create, organize, and track to-do items with support for due dates, subtasks, and task lists. As part of Google Workspace, Google Tasks keeps your action items synchronized across all your devices.
|
||||
|
||||
In Sim, the Google Tasks integration allows your agents to create, read, update, delete, and list tasks and task lists as part of automated workflows. This enables use cases such as automated task creation from incoming data, to-do list management based on workflow triggers, task status tracking, and deadline monitoring. By connecting Sim with Google Tasks, your agents can manage action items programmatically, keep teams organized, and ensure nothing falls through the cracks.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Google Tasks into your workflow. Create, read, update, delete, and list tasks and task lists.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `google_tasks_create`
|
||||
|
||||
Create a new task in a Google Tasks list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
|
||||
| `title` | string | Yes | Title of the task \(max 1024 characters\) |
|
||||
| `notes` | string | No | Notes/description for the task \(max 8192 characters\) |
|
||||
| `due` | string | No | Due date in RFC 3339 format \(e.g., 2025-06-03T00:00:00.000Z\) |
|
||||
| `status` | string | No | Task status: "needsAction" or "completed" |
|
||||
| `parent` | string | No | Parent task ID to create this task as a subtask. Omit for top-level tasks. |
|
||||
| `previous` | string | No | Previous sibling task ID to position after. Omit to place first among siblings. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Task ID |
|
||||
| `title` | string | Task title |
|
||||
| `notes` | string | Task notes |
|
||||
| `status` | string | Task status \(needsAction or completed\) |
|
||||
| `due` | string | Due date |
|
||||
| `updated` | string | Last modification time |
|
||||
| `selfLink` | string | URL for the task |
|
||||
| `webViewLink` | string | Link to task in Google Tasks UI |
|
||||
| `parent` | string | Parent task ID |
|
||||
| `position` | string | Position among sibling tasks |
|
||||
| `completed` | string | Completion date |
|
||||
| `deleted` | boolean | Whether the task is deleted |
|
||||
|
||||
### `google_tasks_list`
|
||||
|
||||
List all tasks in a Google Tasks list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
|
||||
| `maxResults` | number | No | Maximum number of tasks to return \(default 20, max 100\) |
|
||||
| `pageToken` | string | No | Token for pagination |
|
||||
| `showCompleted` | boolean | No | Whether to show completed tasks \(default true\) |
|
||||
| `showDeleted` | boolean | No | Whether to show deleted tasks \(default false\) |
|
||||
| `showHidden` | boolean | No | Whether to show hidden tasks \(default false\) |
|
||||
| `dueMin` | string | No | Lower bound for due date filter \(RFC 3339 timestamp\) |
|
||||
| `dueMax` | string | No | Upper bound for due date filter \(RFC 3339 timestamp\) |
|
||||
| `completedMin` | string | No | Lower bound for task completion date \(RFC 3339 timestamp\) |
|
||||
| `completedMax` | string | No | Upper bound for task completion date \(RFC 3339 timestamp\) |
|
||||
| `updatedMin` | string | No | Lower bound for last modification time \(RFC 3339 timestamp\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tasks` | array | List of tasks |
|
||||
| ↳ `id` | string | Task identifier |
|
||||
| ↳ `title` | string | Title of the task |
|
||||
| ↳ `notes` | string | Notes/description for the task |
|
||||
| ↳ `status` | string | Task status: "needsAction" or "completed" |
|
||||
| ↳ `due` | string | Due date \(RFC 3339 timestamp\) |
|
||||
| ↳ `completed` | string | Completion date \(RFC 3339 timestamp\) |
|
||||
| ↳ `updated` | string | Last modification time \(RFC 3339 timestamp\) |
|
||||
| ↳ `selfLink` | string | URL pointing to this task |
|
||||
| ↳ `webViewLink` | string | Link to task in Google Tasks UI |
|
||||
| ↳ `parent` | string | Parent task identifier |
|
||||
| ↳ `position` | string | Position among sibling tasks \(string-based ordering\) |
|
||||
| ↳ `hidden` | boolean | Whether the task is hidden |
|
||||
| ↳ `deleted` | boolean | Whether the task is deleted |
|
||||
| ↳ `links` | array | Collection of links associated with the task |
|
||||
| ↳ `type` | string | Link type \(e.g., "email", "generic", "chat_message"\) |
|
||||
| ↳ `description` | string | Link description |
|
||||
| ↳ `link` | string | The URL |
|
||||
| `nextPageToken` | string | Token for retrieving the next page of results |
|
||||
|
||||
### `google_tasks_get`
|
||||
|
||||
Retrieve a specific task by ID from a Google Tasks list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
|
||||
| `taskId` | string | Yes | The ID of the task to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Task ID |
|
||||
| `title` | string | Task title |
|
||||
| `notes` | string | Task notes |
|
||||
| `status` | string | Task status \(needsAction or completed\) |
|
||||
| `due` | string | Due date |
|
||||
| `updated` | string | Last modification time |
|
||||
| `selfLink` | string | URL for the task |
|
||||
| `webViewLink` | string | Link to task in Google Tasks UI |
|
||||
| `parent` | string | Parent task ID |
|
||||
| `position` | string | Position among sibling tasks |
|
||||
| `completed` | string | Completion date |
|
||||
| `deleted` | boolean | Whether the task is deleted |
|
||||
|
||||
### `google_tasks_update`
|
||||
|
||||
Update an existing task in a Google Tasks list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
|
||||
| `taskId` | string | Yes | The ID of the task to update |
|
||||
| `title` | string | No | New title for the task |
|
||||
| `notes` | string | No | New notes for the task |
|
||||
| `due` | string | No | New due date in RFC 3339 format |
|
||||
| `status` | string | No | New status: "needsAction" or "completed" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Task ID |
|
||||
| `title` | string | Task title |
|
||||
| `notes` | string | Task notes |
|
||||
| `status` | string | Task status \(needsAction or completed\) |
|
||||
| `due` | string | Due date |
|
||||
| `updated` | string | Last modification time |
|
||||
| `selfLink` | string | URL for the task |
|
||||
| `webViewLink` | string | Link to task in Google Tasks UI |
|
||||
| `parent` | string | Parent task ID |
|
||||
| `position` | string | Position among sibling tasks |
|
||||
| `completed` | string | Completion date |
|
||||
| `deleted` | boolean | Whether the task is deleted |
|
||||
|
||||
### `google_tasks_delete`
|
||||
|
||||
Delete a task from a Google Tasks list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
|
||||
| `taskId` | string | Yes | The ID of the task to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskId` | string | Deleted task ID |
|
||||
| `deleted` | boolean | Whether deletion was successful |
|
||||
|
||||
### `google_tasks_list_task_lists`
|
||||
|
||||
Retrieve all task lists for the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `maxResults` | number | No | Maximum number of task lists to return \(default 20, max 100\) |
|
||||
| `pageToken` | string | No | Token for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskLists` | array | List of task lists |
|
||||
| ↳ `id` | string | Task list identifier |
|
||||
| ↳ `title` | string | Title of the task list |
|
||||
| ↳ `updated` | string | Last modification time \(RFC 3339 timestamp\) |
|
||||
| ↳ `selfLink` | string | URL pointing to this task list |
|
||||
| `nextPageToken` | string | Token for retrieving the next page of results |
|
||||
|
||||
|
||||
60
apps/docs/content/docs/en/tools/google_translate.mdx
Normal file
60
apps/docs/content/docs/en/tools/google_translate.mdx
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Google Translate
|
||||
description: Translate text using Google Cloud Translation
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_translate"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Translate and detect languages using the Google Cloud Translation API. Supports auto-detection of the source language.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `google_translate_text`
|
||||
|
||||
Translate text between languages using the Google Cloud Translation API. Supports auto-detection of the source language.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Cloud API key with Cloud Translation API enabled |
|
||||
| `text` | string | Yes | The text to translate |
|
||||
| `target` | string | Yes | Target language code \(e.g., "es", "fr", "de", "ja"\) |
|
||||
| `source` | string | No | Source language code. If omitted, the API will auto-detect the source language. |
|
||||
| `format` | string | No | Format of the text: "text" for plain text, "html" for HTML content |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `translatedText` | string | The translated text |
|
||||
| `detectedSourceLanguage` | string | The detected source language code \(if source was not specified\) |
|
||||
|
||||
### `google_translate_detect`
|
||||
|
||||
Detect the language of text using the Google Cloud Translation API.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Google Cloud API key with Cloud Translation API enabled |
|
||||
| `text` | string | Yes | The text to detect the language of |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `language` | string | The detected language code \(e.g., "en", "es", "fr"\) |
|
||||
| `confidence` | number | Confidence score of the detection |
|
||||
|
||||
|
||||
459
apps/docs/content/docs/en/tools/hex.mdx
Normal file
459
apps/docs/content/docs/en/tools/hex.mdx
Normal file
@@ -0,0 +1,459 @@
|
||||
---
|
||||
title: Hex
|
||||
description: Run and manage Hex projects
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="hex"
|
||||
color="#14151A"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Hex](https://hex.tech/) is a collaborative platform for analytics and data science that allows you to build, run, and share interactive data projects and notebooks. Hex lets teams work together on data exploration, transformation, and visualization, making it easy to turn analysis into shareable insights.
|
||||
|
||||
With Hex, you can:
|
||||
|
||||
- **Create and run powerful notebooks**: Blend SQL, Python, and visualizations in a single, interactive workspace.
|
||||
- **Collaborate and share**: Work together with teammates in real time and publish interactive data apps for broader audiences.
|
||||
- **Automate and orchestrate workflows**: Schedule notebook runs, parameterize runs with inputs, and automate data tasks.
|
||||
- **Visualize and communicate results**: Turn analysis results into dashboards or interactive apps that anyone can use.
|
||||
- **Integrate with your data stack**: Connect easily to data warehouses, APIs, and other sources.
|
||||
|
||||
The Sim Hex integration allows your AI agents or workflows to:
|
||||
|
||||
- List, get, and manage Hex projects directly from Sim.
|
||||
- Trigger and monitor notebook runs, check their statuses, or cancel them as part of larger automation flows.
|
||||
- Retrieve run results and use them within Sim-powered processes and decision-making.
|
||||
- Leverage Hex’s interactive analytics capabilities right inside your automated Sim workflows.
|
||||
|
||||
Whether you’re empowering analysts, automating reporting, or embedding actionable data into your processes, Hex and Sim provide a seamless way to operationalize analytics and bring data-driven insights to your team.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Hex into your workflow. Run projects, check run status, manage collections and groups, list users, and view data connections. Requires a Hex API token.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `hex_cancel_run`
|
||||
|
||||
Cancel an active Hex project run.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `projectId` | string | Yes | The UUID of the Hex project |
|
||||
| `runId` | string | Yes | The UUID of the run to cancel |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the run was successfully cancelled |
|
||||
| `projectId` | string | Project UUID |
|
||||
| `runId` | string | Run UUID that was cancelled |
|
||||
|
||||
### `hex_create_collection`
|
||||
|
||||
Create a new collection in the Hex workspace to organize projects.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `name` | string | Yes | Name for the new collection |
|
||||
| `description` | string | No | Optional description for the collection |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Newly created collection UUID |
|
||||
| `name` | string | Collection name |
|
||||
| `description` | string | Collection description |
|
||||
| `creator` | object | Collection creator |
|
||||
| ↳ `email` | string | Creator email |
|
||||
| ↳ `id` | string | Creator UUID |
|
||||
|
||||
### `hex_get_collection`
|
||||
|
||||
Retrieve details for a specific Hex collection by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `collectionId` | string | Yes | The UUID of the collection |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Collection UUID |
|
||||
| `name` | string | Collection name |
|
||||
| `description` | string | Collection description |
|
||||
| `creator` | object | Collection creator |
|
||||
| ↳ `email` | string | Creator email |
|
||||
| ↳ `id` | string | Creator UUID |
|
||||
|
||||
### `hex_get_data_connection`
|
||||
|
||||
Retrieve details for a specific data connection including type, description, and configuration flags.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `dataConnectionId` | string | Yes | The UUID of the data connection |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Connection UUID |
|
||||
| `name` | string | Connection name |
|
||||
| `type` | string | Connection type \(e.g., snowflake, postgres, bigquery\) |
|
||||
| `description` | string | Connection description |
|
||||
| `connectViaSsh` | boolean | Whether SSH tunneling is enabled |
|
||||
| `includeMagic` | boolean | Whether Magic AI features are enabled |
|
||||
| `allowWritebackCells` | boolean | Whether writeback cells are allowed |
|
||||
|
||||
### `hex_get_group`
|
||||
|
||||
Retrieve details for a specific Hex group.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `groupId` | string | Yes | The UUID of the group |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Group UUID |
|
||||
| `name` | string | Group name |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
|
||||
### `hex_get_project`
|
||||
|
||||
Get metadata and details for a specific Hex project by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `projectId` | string | Yes | The UUID of the Hex project |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Project UUID |
|
||||
| `title` | string | Project title |
|
||||
| `description` | string | Project description |
|
||||
| `status` | object | Project status |
|
||||
| ↳ `name` | string | Status name \(e.g., PUBLISHED, DRAFT\) |
|
||||
| `type` | string | Project type \(PROJECT or COMPONENT\) |
|
||||
| `creator` | object | Project creator |
|
||||
| ↳ `email` | string | Creator email |
|
||||
| `owner` | object | Project owner |
|
||||
| ↳ `email` | string | Owner email |
|
||||
| `categories` | array | Project categories |
|
||||
| ↳ `name` | string | Category name |
|
||||
| ↳ `description` | string | Category description |
|
||||
| `lastEditedAt` | string | ISO 8601 last edited timestamp |
|
||||
| `lastPublishedAt` | string | ISO 8601 last published timestamp |
|
||||
| `createdAt` | string | ISO 8601 creation timestamp |
|
||||
| `archivedAt` | string | ISO 8601 archived timestamp |
|
||||
| `trashedAt` | string | ISO 8601 trashed timestamp |
|
||||
|
||||
### `hex_get_project_runs`
|
||||
|
||||
Retrieve API-triggered runs for a Hex project with optional filtering by status and pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `projectId` | string | Yes | The UUID of the Hex project |
|
||||
| `limit` | number | No | Maximum number of runs to return \(1-100, default: 25\) |
|
||||
| `offset` | number | No | Offset for paginated results \(default: 0\) |
|
||||
| `statusFilter` | string | No | Filter by run status: PENDING, RUNNING, ERRORED, COMPLETED, KILLED, UNABLE_TO_ALLOCATE_KERNEL |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `runs` | array | List of project runs |
|
||||
| ↳ `projectId` | string | Project UUID |
|
||||
| ↳ `runId` | string | Run UUID |
|
||||
| ↳ `runUrl` | string | URL to view the run |
|
||||
| ↳ `status` | string | Run status \(PENDING, RUNNING, COMPLETED, ERRORED, KILLED, UNABLE_TO_ALLOCATE_KERNEL\) |
|
||||
| ↳ `startTime` | string | Run start time |
|
||||
| ↳ `endTime` | string | Run end time |
|
||||
| ↳ `elapsedTime` | number | Elapsed time in seconds |
|
||||
| ↳ `traceId` | string | Trace ID |
|
||||
| ↳ `projectVersion` | number | Project version number |
|
||||
| `total` | number | Total number of runs returned |
|
||||
| `traceId` | string | Top-level trace ID |
|
||||
|
||||
### `hex_get_queried_tables`
|
||||
|
||||
Return the warehouse tables queried by a Hex project, including data connection and table names.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `projectId` | string | Yes | The UUID of the Hex project |
|
||||
| `limit` | number | No | Maximum number of tables to return \(1-100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tables` | array | List of warehouse tables queried by the project |
|
||||
| ↳ `dataConnectionId` | string | Data connection UUID |
|
||||
| ↳ `dataConnectionName` | string | Data connection name |
|
||||
| ↳ `tableName` | string | Table name |
|
||||
| `total` | number | Total number of tables returned |
|
||||
|
||||
### `hex_get_run_status`
|
||||
|
||||
Check the status of a Hex project run by its run ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `projectId` | string | Yes | The UUID of the Hex project |
|
||||
| `runId` | string | Yes | The UUID of the run to check |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projectId` | string | Project UUID |
|
||||
| `runId` | string | Run UUID |
|
||||
| `runUrl` | string | URL to view the run |
|
||||
| `status` | string | Run status \(PENDING, RUNNING, COMPLETED, ERRORED, KILLED, UNABLE_TO_ALLOCATE_KERNEL\) |
|
||||
| `startTime` | string | ISO 8601 run start time |
|
||||
| `endTime` | string | ISO 8601 run end time |
|
||||
| `elapsedTime` | number | Elapsed time in seconds |
|
||||
| `traceId` | string | Trace ID for debugging |
|
||||
| `projectVersion` | number | Project version number |
|
||||
|
||||
### `hex_list_collections`
|
||||
|
||||
List all collections in the Hex workspace.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `limit` | number | No | Maximum number of collections to return \(1-500, default: 25\) |
|
||||
| `sortBy` | string | No | Sort by field: NAME |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collections` | array | List of collections |
|
||||
| ↳ `id` | string | Collection UUID |
|
||||
| ↳ `name` | string | Collection name |
|
||||
| ↳ `description` | string | Collection description |
|
||||
| ↳ `creator` | object | Collection creator |
|
||||
| ↳ `email` | string | Creator email |
|
||||
| ↳ `id` | string | Creator UUID |
|
||||
| `total` | number | Total number of collections returned |
|
||||
|
||||
### `hex_list_data_connections`
|
||||
|
||||
List all data connections in the Hex workspace (e.g., Snowflake, PostgreSQL, BigQuery).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `limit` | number | No | Maximum number of connections to return \(1-500, default: 25\) |
|
||||
| `sortBy` | string | No | Sort by field: CREATED_AT or NAME |
|
||||
| `sortDirection` | string | No | Sort direction: ASC or DESC |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `connections` | array | List of data connections |
|
||||
| ↳ `id` | string | Connection UUID |
|
||||
| ↳ `name` | string | Connection name |
|
||||
| ↳ `type` | string | Connection type \(e.g., athena, bigquery, databricks, postgres, redshift, snowflake\) |
|
||||
| ↳ `description` | string | Connection description |
|
||||
| ↳ `connectViaSsh` | boolean | Whether SSH tunneling is enabled |
|
||||
| ↳ `includeMagic` | boolean | Whether Magic AI features are enabled |
|
||||
| ↳ `allowWritebackCells` | boolean | Whether writeback cells are allowed |
|
||||
| `total` | number | Total number of connections returned |
|
||||
|
||||
### `hex_list_groups`
|
||||
|
||||
List all groups in the Hex workspace with optional sorting.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `limit` | number | No | Maximum number of groups to return \(1-500, default: 25\) |
|
||||
| `sortBy` | string | No | Sort by field: CREATED_AT or NAME |
|
||||
| `sortDirection` | string | No | Sort direction: ASC or DESC |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `groups` | array | List of workspace groups |
|
||||
| ↳ `id` | string | Group UUID |
|
||||
| ↳ `name` | string | Group name |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| `total` | number | Total number of groups returned |
|
||||
|
||||
### `hex_list_projects`
|
||||
|
||||
List all projects in your Hex workspace with optional filtering by status.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `limit` | number | No | Maximum number of projects to return \(1-100\) |
|
||||
| `includeArchived` | boolean | No | Include archived projects in results |
|
||||
| `statusFilter` | string | No | Filter by status: PUBLISHED, DRAFT, or ALL |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projects` | array | List of Hex projects |
|
||||
| ↳ `id` | string | Project UUID |
|
||||
| ↳ `title` | string | Project title |
|
||||
| ↳ `description` | string | Project description |
|
||||
| ↳ `status` | object | Project status |
|
||||
| ↳ `name` | string | Status name \(e.g., PUBLISHED, DRAFT\) |
|
||||
| ↳ `type` | string | Project type \(PROJECT or COMPONENT\) |
|
||||
| ↳ `creator` | object | Project creator |
|
||||
| ↳ `email` | string | Creator email |
|
||||
| ↳ `owner` | object | Project owner |
|
||||
| ↳ `email` | string | Owner email |
|
||||
| ↳ `lastEditedAt` | string | Last edited timestamp |
|
||||
| ↳ `lastPublishedAt` | string | Last published timestamp |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `archivedAt` | string | Archived timestamp |
|
||||
| `total` | number | Total number of projects returned |
|
||||
|
||||
### `hex_list_users`
|
||||
|
||||
List all users in the Hex workspace with optional filtering and sorting.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `limit` | number | No | Maximum number of users to return \(1-100, default: 25\) |
|
||||
| `sortBy` | string | No | Sort by field: NAME or EMAIL |
|
||||
| `sortDirection` | string | No | Sort direction: ASC or DESC |
|
||||
| `groupId` | string | No | Filter users by group UUID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | List of workspace users |
|
||||
| ↳ `id` | string | User UUID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `role` | string | User role \(ADMIN, MANAGER, EDITOR, EXPLORER, MEMBER, GUEST, EMBEDDED_USER, ANONYMOUS\) |
|
||||
| `total` | number | Total number of users returned |
|
||||
|
||||
### `hex_run_project`
|
||||
|
||||
Execute a published Hex project. Optionally pass input parameters and control caching behavior.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `projectId` | string | Yes | The UUID of the Hex project to run |
|
||||
| `inputParams` | json | No | JSON object of input parameters for the project \(e.g., \{"date": "2024-01-01"\}\) |
|
||||
| `dryRun` | boolean | No | If true, perform a dry run without executing the project |
|
||||
| `updateCache` | boolean | No | \(Deprecated\) If true, update the cached results after execution |
|
||||
| `updatePublishedResults` | boolean | No | If true, update the published app results after execution |
|
||||
| `useCachedSqlResults` | boolean | No | If true, use cached SQL results instead of re-running queries |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projectId` | string | Project UUID |
|
||||
| `runId` | string | Run UUID |
|
||||
| `runUrl` | string | URL to view the run |
|
||||
| `runStatusUrl` | string | URL to check run status |
|
||||
| `traceId` | string | Trace ID for debugging |
|
||||
| `projectVersion` | number | Project version number |
|
||||
|
||||
### `hex_update_project`
|
||||
|
||||
Update a Hex project status label (e.g., endorsement or custom workspace statuses).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Hex API token \(Personal or Workspace\) |
|
||||
| `projectId` | string | Yes | The UUID of the Hex project to update |
|
||||
| `status` | string | Yes | New project status name \(custom workspace status label\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Project UUID |
|
||||
| `title` | string | Project title |
|
||||
| `description` | string | Project description |
|
||||
| `status` | object | Updated project status |
|
||||
| ↳ `name` | string | Status name \(e.g., PUBLISHED, DRAFT\) |
|
||||
| `type` | string | Project type \(PROJECT or COMPONENT\) |
|
||||
| `creator` | object | Project creator |
|
||||
| ↳ `email` | string | Creator email |
|
||||
| `owner` | object | Project owner |
|
||||
| ↳ `email` | string | Owner email |
|
||||
| `categories` | array | Project categories |
|
||||
| ↳ `name` | string | Category name |
|
||||
| ↳ `description` | string | Category description |
|
||||
| `lastEditedAt` | string | Last edited timestamp |
|
||||
| `lastPublishedAt` | string | Last published timestamp |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `archivedAt` | string | Archived timestamp |
|
||||
| `trashedAt` | string | Trashed timestamp |
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ Create a new service request in Jira Service Management
|
||||
| `summary` | string | Yes | Summary/title for the service request |
|
||||
| `description` | string | No | Description for the service request |
|
||||
| `raiseOnBehalfOf` | string | No | Account ID of customer to raise request on behalf of |
|
||||
| `requestFieldValues` | json | No | Custom field values as key-value pairs \(overrides summary/description if provided\) |
|
||||
| `requestFieldValues` | json | No | Request field values as key-value pairs \(overrides summary/description if provided\) |
|
||||
| `requestParticipants` | string | No | Comma-separated account IDs to add as request participants |
|
||||
| `channel` | string | No | Channel the request originates from \(e.g., portal, email\) |
|
||||
|
||||
|
||||
@@ -5,19 +5,23 @@
|
||||
"ahrefs",
|
||||
"airtable",
|
||||
"airweave",
|
||||
"algolia",
|
||||
"apify",
|
||||
"apollo",
|
||||
"arxiv",
|
||||
"asana",
|
||||
"attio",
|
||||
"browser_use",
|
||||
"calcom",
|
||||
"calendly",
|
||||
"circleback",
|
||||
"clay",
|
||||
"clerk",
|
||||
"cloudflare",
|
||||
"confluence",
|
||||
"cursor",
|
||||
"datadog",
|
||||
"devin",
|
||||
"discord",
|
||||
"dropbox",
|
||||
"dspy",
|
||||
@@ -33,6 +37,8 @@
|
||||
"github",
|
||||
"gitlab",
|
||||
"gmail",
|
||||
"gong",
|
||||
"google_bigquery",
|
||||
"google_books",
|
||||
"google_calendar",
|
||||
"google_docs",
|
||||
@@ -43,10 +49,13 @@
|
||||
"google_search",
|
||||
"google_sheets",
|
||||
"google_slides",
|
||||
"google_tasks",
|
||||
"google_translate",
|
||||
"google_vault",
|
||||
"grafana",
|
||||
"grain",
|
||||
"greptile",
|
||||
"hex",
|
||||
"hubspot",
|
||||
"huggingface",
|
||||
"hunter",
|
||||
@@ -68,6 +77,7 @@
|
||||
"mailgun",
|
||||
"mem0",
|
||||
"memory",
|
||||
"microsoft_dataverse",
|
||||
"microsoft_excel",
|
||||
"microsoft_planner",
|
||||
"microsoft_teams",
|
||||
@@ -91,8 +101,10 @@
|
||||
"qdrant",
|
||||
"rds",
|
||||
"reddit",
|
||||
"redis",
|
||||
"reducto",
|
||||
"resend",
|
||||
"revenuecat",
|
||||
"s3",
|
||||
"salesforce",
|
||||
"search",
|
||||
@@ -112,6 +124,7 @@
|
||||
"stripe",
|
||||
"stt",
|
||||
"supabase",
|
||||
"table",
|
||||
"tavily",
|
||||
"telegram",
|
||||
"textract",
|
||||
@@ -122,6 +135,8 @@
|
||||
"twilio_sms",
|
||||
"twilio_voice",
|
||||
"typeform",
|
||||
"upstash",
|
||||
"vercel",
|
||||
"video_generator",
|
||||
"vision",
|
||||
"wealthbox",
|
||||
|
||||
426
apps/docs/content/docs/en/tools/microsoft_dataverse.mdx
Normal file
426
apps/docs/content/docs/en/tools/microsoft_dataverse.mdx
Normal file
@@ -0,0 +1,426 @@
|
||||
---
|
||||
title: Microsoft Dataverse
|
||||
description: Manage records in Microsoft Dataverse tables
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="microsoft_dataverse"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Microsoft Dataverse](https://learn.microsoft.com/en-us/power-apps/maker/data-platform/data-platform-intro) is a powerful cloud data platform for securely storing, managing, and interacting with structured business data. The Microsoft Dataverse integration enables you to programmatically create, read, update, delete, and link records in Dataverse tables as part of your workflow and automation needs.
|
||||
|
||||
With Microsoft Dataverse integration, you can:
|
||||
|
||||
- **List and query records:** Access lists of records or query with advanced filters to find the data you need from any Dataverse table.
|
||||
- **Create and update records:** Add new records or update existing ones in any table for use across Power Platform, Dynamics 365, and custom apps.
|
||||
- **Delete and manage records:** Remove records as part of data lifecycle management directly from your automation flows.
|
||||
- **Associate and disassociate records:** Link related items together or remove associations using entity relationships and navigation properties—essential for reflecting complex business processes.
|
||||
- **Work with any Dataverse environment:** Connect to your organization’s environments, including production, sandbox, or Dynamics 365 tenants, for maximum flexibility.
|
||||
- **Integrate with Power Platform and Dynamics 365:** Automate tasks ranging from sales and marketing data updates to custom app workflows—all powered by Dataverse's security and governance.
|
||||
|
||||
The Dataverse integration empowers solution builders and business users to automate business processes, maintain accurate and up-to-date information, create system integrations, trigger actions, and drive insights—all with robust security and governance.
|
||||
|
||||
Connect Microsoft Dataverse to your automations to unlock sophisticated data management, orchestration, and business logic across your apps, teams, and cloud services.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, and relevance search. Works with Dynamics 365, Power Platform, and custom Dataverse environments.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `microsoft_dataverse_associate`
|
||||
|
||||
Associate two records in Microsoft Dataverse via a navigation property. Creates a relationship between a source record and a target record. Supports both collection-valued (POST) and single-valued (PUT) navigation properties.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Source entity set name \(e.g., accounts\) |
|
||||
| `recordId` | string | Yes | Source record GUID |
|
||||
| `navigationProperty` | string | Yes | Navigation property name \(e.g., contact_customer_accounts for collection-valued, or parentcustomerid_account for single-valued\) |
|
||||
| `targetEntitySetName` | string | Yes | Target entity set name \(e.g., contacts\) |
|
||||
| `targetRecordId` | string | Yes | Target record GUID to associate |
|
||||
| `navigationType` | string | No | Type of navigation property: "collection" \(default, uses POST\) or "single" \(uses PUT for lookup fields\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the association was created successfully |
|
||||
| `entitySetName` | string | Source entity set name used in the association |
|
||||
| `recordId` | string | Source record GUID that was associated |
|
||||
| `navigationProperty` | string | Navigation property used for the association |
|
||||
| `targetEntitySetName` | string | Target entity set name used in the association |
|
||||
| `targetRecordId` | string | Target record GUID that was associated |
|
||||
|
||||
### `microsoft_dataverse_create_multiple`
|
||||
|
||||
Create multiple records of the same table type in a single request. Each record in the Targets array must include an @odata.type annotation. Recommended batch size: 100-1000 records for standard tables.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `entityLogicalName` | string | Yes | Table logical name for @odata.type annotation \(e.g., account, contact\). Used to set Microsoft.Dynamics.CRM.\{entityLogicalName\} on each record. |
|
||||
| `records` | object | Yes | Array of record objects to create. Each record should contain column logical names as keys. The @odata.type annotation is added automatically. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ids` | array | Array of GUIDs for the created records |
|
||||
| `count` | number | Number of records created |
|
||||
| `success` | boolean | Whether all records were created successfully |
|
||||
|
||||
### `microsoft_dataverse_create_record`
|
||||
|
||||
Create a new record in a Microsoft Dataverse table. Requires the entity set name (plural table name) and record data as a JSON object.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `data` | object | Yes | Record data as a JSON object with column names as keys |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recordId` | string | The ID of the created record |
|
||||
| `record` | object | Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields. |
|
||||
| `success` | boolean | Whether the record was created successfully |
|
||||
|
||||
### `microsoft_dataverse_delete_record`
|
||||
|
||||
Delete a record from a Microsoft Dataverse table by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recordId` | string | The ID of the deleted record |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_disassociate`
|
||||
|
||||
Remove an association between two records in Microsoft Dataverse. For collection-valued navigation properties, provide the target record ID. For single-valued navigation properties, only the navigation property name is needed.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Source entity set name \(e.g., accounts\) |
|
||||
| `recordId` | string | Yes | Source record GUID |
|
||||
| `navigationProperty` | string | Yes | Navigation property name \(e.g., contact_customer_accounts for collection-valued, or parentcustomerid_account for single-valued\) |
|
||||
| `targetRecordId` | string | No | Target record GUID \(required for collection-valued navigation properties, omit for single-valued\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the disassociation was completed successfully |
|
||||
| `entitySetName` | string | Source entity set name used in the disassociation |
|
||||
| `recordId` | string | Source record GUID that was disassociated |
|
||||
| `navigationProperty` | string | Navigation property used for the disassociation |
|
||||
| `targetRecordId` | string | Target record GUID that was disassociated |
|
||||
|
||||
### `microsoft_dataverse_download_file`
|
||||
|
||||
Download a file from a file or image column on a Dataverse record. Returns the file content as a base64-encoded string along with file metadata from response headers.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | Record GUID to download the file from |
|
||||
| `fileColumn` | string | Yes | File or image column logical name \(e.g., entityimage, cr_document\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `fileContent` | string | Base64-encoded file content |
|
||||
| `fileName` | string | Name of the downloaded file |
|
||||
| `fileSize` | number | File size in bytes |
|
||||
| `mimeType` | string | MIME type of the file |
|
||||
| `success` | boolean | Whether the file was downloaded successfully |
|
||||
|
||||
### `microsoft_dataverse_execute_action`
|
||||
|
||||
Execute a bound or unbound Dataverse action. Actions perform operations with side effects (e.g., Merge, GrantAccess, SendEmail, QualifyLead). For bound actions, provide the entity set name and record ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `actionName` | string | Yes | Action name \(e.g., Merge, GrantAccess, SendEmail\). Do not include the Microsoft.Dynamics.CRM. namespace prefix for unbound actions. |
|
||||
| `entitySetName` | string | No | Entity set name for bound actions \(e.g., accounts\). Leave empty for unbound actions. |
|
||||
| `recordId` | string | No | Record GUID for bound actions. Leave empty for unbound or collection-bound actions. |
|
||||
| `parameters` | object | No | Action parameters as a JSON object. For entity references, include @odata.type annotation \(e.g., \{"Target": \{"@odata.type": "Microsoft.Dynamics.CRM.account", "accountid": "..."\}\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `result` | object | Action response data. Structure varies by action. Null for actions that return 204 No Content. |
|
||||
| `success` | boolean | Whether the action executed successfully |
|
||||
|
||||
### `microsoft_dataverse_execute_function`
|
||||
|
||||
Execute a bound or unbound Dataverse function. Functions are read-only operations (e.g., RetrievePrincipalAccess, RetrieveTotalRecordCount, InitializeFrom). For bound functions, provide the entity set name and record ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `functionName` | string | Yes | Function name \(e.g., RetrievePrincipalAccess, RetrieveTotalRecordCount\). Do not include the Microsoft.Dynamics.CRM. namespace prefix for unbound functions. |
|
||||
| `entitySetName` | string | No | Entity set name for bound functions \(e.g., systemusers\). Leave empty for unbound functions. |
|
||||
| `recordId` | string | No | Record GUID for bound functions. Leave empty for unbound functions. |
|
||||
| `parameters` | string | No | Function parameters as a comma-separated list of name=value pairs for the URL \(e.g., "LocalizedStandardName=\'Pacific Standard Time\ |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `result` | object | Function response data. Structure varies by function. |
|
||||
| `success` | boolean | Whether the function executed successfully |
|
||||
|
||||
### `microsoft_dataverse_fetchxml_query`
|
||||
|
||||
Execute a FetchXML query against a Microsoft Dataverse table. FetchXML supports aggregation, grouping, linked-entity joins, and complex filtering beyond OData capabilities.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `fetchXml` | string | Yes | FetchXML query string. Must include <fetch> root element and <entity> child element matching the table logical name. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | Array of Dataverse records. Each record has dynamic columns based on the table schema. |
|
||||
| `count` | number | Number of records returned in the current page |
|
||||
| `fetchXmlPagingCookie` | string | Paging cookie for retrieving the next page of results |
|
||||
| `moreRecords` | boolean | Whether more records are available beyond the current page |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_get_record`
|
||||
|
||||
Retrieve a single record from a Microsoft Dataverse table by its ID. Supports $select and $expand OData query options.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to retrieve |
|
||||
| `select` | string | No | Comma-separated list of columns to return \(OData $select\) |
|
||||
| `expand` | string | No | Navigation properties to expand \(OData $expand\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | object | Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields. |
|
||||
| `recordId` | string | The record primary key ID \(auto-detected from response\) |
|
||||
| `success` | boolean | Whether the record was retrieved successfully |
|
||||
|
||||
### `microsoft_dataverse_list_records`
|
||||
|
||||
Query and list records from a Microsoft Dataverse table. Supports OData query options for filtering, selecting columns, ordering, and pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `select` | string | No | Comma-separated list of columns to return \(OData $select\) |
|
||||
| `filter` | string | No | OData $filter expression \(e.g., statecode eq 0\) |
|
||||
| `orderBy` | string | No | OData $orderby expression \(e.g., name asc, createdon desc\) |
|
||||
| `top` | number | No | Maximum number of records to return \(OData $top\) |
|
||||
| `expand` | string | No | Navigation properties to expand \(OData $expand\) |
|
||||
| `count` | string | No | Set to "true" to include total record count in response \(OData $count\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | Array of Dataverse records. Each record has dynamic columns based on the table schema. |
|
||||
| `count` | number | Number of records returned in the current page |
|
||||
| `totalCount` | number | Total number of matching records server-side \(requires $count=true\) |
|
||||
| `nextLink` | string | URL for the next page of results |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_search`
|
||||
|
||||
Perform a full-text relevance search across Microsoft Dataverse tables. Requires Dataverse Search to be enabled on the environment. Supports simple and Lucene query syntax.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `searchTerm` | string | Yes | Search text \(1-100 chars\). Supports simple syntax: + \(AND\), \| \(OR\), - \(NOT\), * \(wildcard\), "exact phrase" |
|
||||
| `entities` | string | No | JSON array of search entity configs. Each object: \{"Name":"account","SelectColumns":\["name"\],"SearchColumns":\["name"\],"Filter":"statecode eq 0"\} |
|
||||
| `filter` | string | No | Global OData filter applied across all entities \(e.g., "createdon gt 2024-01-01"\) |
|
||||
| `facets` | string | No | JSON array of facet specifications \(e.g., \["entityname,count:100","ownerid,count:100"\]\) |
|
||||
| `top` | number | No | Maximum number of results \(default: 50, max: 100\) |
|
||||
| `skip` | number | No | Number of results to skip for pagination |
|
||||
| `orderBy` | string | No | JSON array of sort expressions \(e.g., \["createdon desc"\]\) |
|
||||
| `searchMode` | string | No | Search mode: "any" \(default, match any term\) or "all" \(match all terms\) |
|
||||
| `searchType` | string | No | Query type: "simple" \(default\) or "lucene" \(enables regex, fuzzy, proximity, boosting\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | array | Array of search result objects |
|
||||
| ↳ `Id` | string | Record GUID |
|
||||
| ↳ `EntityName` | string | Table logical name \(e.g., account, contact\) |
|
||||
| ↳ `ObjectTypeCode` | number | Entity type code |
|
||||
| ↳ `Attributes` | object | Record attributes matching the search. Keys are column logical names. |
|
||||
| ↳ `Highlights` | object | Highlighted search matches. Keys are column names, values are arrays of strings with \{crmhit\}/\{/crmhit\} markers. |
|
||||
| ↳ `Score` | number | Relevance score for this result |
|
||||
| `totalCount` | number | Total number of matching records across all tables |
|
||||
| `count` | number | Number of results returned in this page |
|
||||
| `facets` | object | Facet results when facets were requested. Keys are facet names, values are arrays of facet value objects with count and value properties. |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_update_multiple`
|
||||
|
||||
Update multiple records of the same table type in a single request. Each record must include its primary key. Only include columns that need to be changed. Recommended batch size: 100-1000 records.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `entityLogicalName` | string | Yes | Table logical name for @odata.type annotation \(e.g., account, contact\). Used to set Microsoft.Dynamics.CRM.\{entityLogicalName\} on each record. |
|
||||
| `records` | object | Yes | Array of record objects to update. Each record must include its primary key \(e.g., accountid\) and only the columns being changed. The @odata.type annotation is added automatically. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether all records were updated successfully |
|
||||
|
||||
### `microsoft_dataverse_update_record`
|
||||
|
||||
Update an existing record in a Microsoft Dataverse table. Only send the columns you want to change.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to update |
|
||||
| `data` | object | Yes | Record data to update as a JSON object with column names as keys |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recordId` | string | The ID of the updated record |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_upload_file`
|
||||
|
||||
Upload a file to a file or image column on a Dataverse record. Supports single-request upload for files up to 128 MB. The file content must be provided as a base64-encoded string.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | Record GUID to upload the file to |
|
||||
| `fileColumn` | string | Yes | File or image column logical name \(e.g., entityimage, cr_document\) |
|
||||
| `fileName` | string | Yes | Name of the file being uploaded \(e.g., document.pdf\) |
|
||||
| `file` | file | No | File to upload \(UserFile object\) |
|
||||
| `fileContent` | string | No | Base64-encoded file content \(legacy\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recordId` | string | Record GUID the file was uploaded to |
|
||||
| `fileColumn` | string | File column the file was uploaded to |
|
||||
| `fileName` | string | Name of the uploaded file |
|
||||
| `success` | boolean | Whether the file was uploaded successfully |
|
||||
|
||||
### `microsoft_dataverse_upsert_record`
|
||||
|
||||
Create or update a record in a Microsoft Dataverse table. If a record with the given ID exists, it is updated; otherwise, a new record is created.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to upsert |
|
||||
| `data` | object | Yes | Record data as a JSON object with column names as keys |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recordId` | string | The ID of the upserted record |
|
||||
| `created` | boolean | True if the record was created, false if updated |
|
||||
| `record` | object | Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields. |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_whoami`
|
||||
|
||||
Retrieve the current authenticated user information from Microsoft Dataverse. Useful for testing connectivity and getting the user ID, business unit ID, and organization ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `userId` | string | The authenticated user ID |
|
||||
| `businessUnitId` | string | The business unit ID |
|
||||
| `organizationId` | string | The organization ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
|
||||
452
apps/docs/content/docs/en/tools/redis.mdx
Normal file
452
apps/docs/content/docs/en/tools/redis.mdx
Normal file
@@ -0,0 +1,452 @@
|
||||
---
|
||||
title: Redis
|
||||
description: Key-value operations with Redis
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="redis"
|
||||
color="#FF4438"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Redis](https://redis.io/) is an open-source, in-memory data structure store, used as a distributed key-value database, cache, and message broker. Redis supports a variety of data structures including strings, hashes, lists, sets, and more, making it highly flexible for different application scenarios.
|
||||
|
||||
With Redis, you can:
|
||||
|
||||
- **Store and retrieve key-value data instantly**: Use Redis as a fast database, cache, or session store for high performance.
|
||||
- **Work with multiple data structures**: Manage not just strings, but also lists, hashes, sets, sorted sets, streams, and bitmaps.
|
||||
- **Perform atomic operations**: Safely manipulate data using atomic commands and transactions.
|
||||
- **Support pub/sub messaging**: Use Redis’s publisher/subscriber features for real-time event handling and messaging.
|
||||
- **Set automatic expiration policies**: Assign TTLs to keys for caching and time-sensitive data.
|
||||
- **Scale horizontally**: Use Redis Cluster for sharding, high availability, and scalable workloads.
|
||||
|
||||
In Sim, the Redis integration lets your agents connect to any Redis-compatible instance to perform key-value, hash, list, and utility operations. You can build workflows that involve storing, retrieving, or manipulating data in Redis, or manage your app’s cache, sessions, or real-time messaging, directly within your Sim workspace.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect to any Redis instance to perform key-value, hash, list, and utility operations via a direct connection.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `redis_get`
|
||||
|
||||
Get the value of a key from Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The key to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was retrieved |
|
||||
| `value` | string | The value of the key, or null if the key does not exist |
|
||||
|
||||
### `redis_set`
|
||||
|
||||
Set the value of a key in Redis with an optional expiration time in seconds.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The key to set |
|
||||
| `value` | string | Yes | The value to store |
|
||||
| `ex` | number | No | Expiration time in seconds \(optional\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was set |
|
||||
| `result` | string | The result of the SET operation \(typically "OK"\) |
|
||||
|
||||
### `redis_delete`
|
||||
|
||||
Delete a key from Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The key to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was deleted |
|
||||
| `deletedCount` | number | Number of keys deleted \(0 if key did not exist, 1 if deleted\) |
|
||||
|
||||
### `redis_keys`
|
||||
|
||||
List all keys matching a pattern in Redis. Avoid using on large databases in production; use the Redis Command tool with SCAN for large key spaces.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `pattern` | string | No | Pattern to match keys \(default: * for all keys\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `pattern` | string | The pattern used to match keys |
|
||||
| `keys` | array | List of keys matching the pattern |
|
||||
| `count` | number | Number of keys found |
|
||||
|
||||
### `redis_command`
|
||||
|
||||
Execute a raw Redis command as a JSON array (e.g. [
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `command` | string | Yes | Redis command as a JSON array \(e.g. \["SET", "key", "value"\]\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `command` | string | The command that was executed |
|
||||
| `result` | json | The result of the command |
|
||||
|
||||
### `redis_hset`
|
||||
|
||||
Set a field in a hash stored at a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The hash key |
|
||||
| `field` | string | Yes | The field name within the hash |
|
||||
| `value` | string | Yes | The value to set for the field |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The hash key |
|
||||
| `field` | string | The field that was set |
|
||||
| `result` | number | Number of fields added \(1 if new, 0 if updated\) |
|
||||
|
||||
### `redis_hget`
|
||||
|
||||
Get the value of a field in a hash stored at a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The hash key |
|
||||
| `field` | string | Yes | The field name to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The hash key |
|
||||
| `field` | string | The field that was retrieved |
|
||||
| `value` | string | The field value, or null if the field or key does not exist |
|
||||
|
||||
### `redis_hgetall`
|
||||
|
||||
Get all fields and values of a hash stored at a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The hash key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The hash key |
|
||||
| `fields` | object | All field-value pairs in the hash as a key-value object. Empty object if the key does not exist. |
|
||||
| `fieldCount` | number | Number of fields in the hash |
|
||||
|
||||
### `redis_hdel`
|
||||
|
||||
Delete a field from a hash stored at a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The hash key |
|
||||
| `field` | string | Yes | The field name to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The hash key |
|
||||
| `field` | string | The field that was deleted |
|
||||
| `deleted` | number | Number of fields removed \(1 if deleted, 0 if field did not exist\) |
|
||||
|
||||
### `redis_incr`
|
||||
|
||||
Increment the integer value of a key by one in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The key to increment |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was incremented |
|
||||
| `value` | number | The new value after increment |
|
||||
|
||||
### `redis_incrby`
|
||||
|
||||
Increment the integer value of a key by a given amount in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The key to increment |
|
||||
| `increment` | number | Yes | Amount to increment by \(negative to decrement\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was incremented |
|
||||
| `value` | number | The new value after increment |
|
||||
|
||||
### `redis_expire`
|
||||
|
||||
Set an expiration time (in seconds) on a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The key to set expiration on |
|
||||
| `seconds` | number | Yes | Timeout in seconds |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that expiration was set on |
|
||||
| `result` | number | 1 if the timeout was set, 0 if the key does not exist |
|
||||
|
||||
### `redis_ttl`
|
||||
|
||||
Get the remaining time to live (in seconds) of a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The key to check TTL for |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was checked |
|
||||
| `ttl` | number | Remaining TTL in seconds. Positive integer if TTL set, -1 if no expiration, -2 if key does not exist. |
|
||||
|
||||
### `redis_persist`
|
||||
|
||||
Remove the expiration from a key in Redis, making it persist indefinitely.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The key to persist |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was persisted |
|
||||
| `result` | number | 1 if the expiration was removed, 0 if the key does not exist or has no expiration |
|
||||
|
||||
### `redis_lpush`
|
||||
|
||||
Prepend a value to a list stored at a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The list key |
|
||||
| `value` | string | Yes | The value to prepend |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The list key |
|
||||
| `length` | number | Length of the list after the push |
|
||||
|
||||
### `redis_rpush`
|
||||
|
||||
Append a value to the end of a list stored at a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The list key |
|
||||
| `value` | string | Yes | The value to append |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The list key |
|
||||
| `length` | number | Length of the list after the push |
|
||||
|
||||
### `redis_lpop`
|
||||
|
||||
Remove and return the first element of a list stored at a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The list key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The list key |
|
||||
| `value` | string | The removed element, or null if the list is empty |
|
||||
|
||||
### `redis_rpop`
|
||||
|
||||
Remove and return the last element of a list stored at a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The list key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The list key |
|
||||
| `value` | string | The removed element, or null if the list is empty |
|
||||
|
||||
### `redis_llen`
|
||||
|
||||
Get the length of a list stored at a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The list key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The list key |
|
||||
| `length` | number | The length of the list, or 0 if the key does not exist |
|
||||
|
||||
### `redis_lrange`
|
||||
|
||||
Get a range of elements from a list stored at a key in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The list key |
|
||||
| `start` | number | Yes | Start index \(0-based\) |
|
||||
| `stop` | number | Yes | Stop index \(-1 for all elements\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The list key |
|
||||
| `values` | array | List elements in the specified range |
|
||||
| `count` | number | Number of elements returned |
|
||||
|
||||
### `redis_exists`
|
||||
|
||||
Check if a key exists in Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The key to check |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was checked |
|
||||
| `exists` | boolean | Whether the key exists \(true\) or not \(false\) |
|
||||
|
||||
### `redis_setnx`
|
||||
|
||||
Set the value of a key in Redis only if the key does not already exist.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `url` | string | Yes | Redis connection URL \(e.g. redis://user:password@host:port\) |
|
||||
| `key` | string | Yes | The key to set |
|
||||
| `value` | string | Yes | The value to store |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was set |
|
||||
| `wasSet` | boolean | Whether the key was set \(true\) or already existed \(false\) |
|
||||
|
||||
|
||||
456
apps/docs/content/docs/en/tools/revenuecat.mdx
Normal file
456
apps/docs/content/docs/en/tools/revenuecat.mdx
Normal file
@@ -0,0 +1,456 @@
|
||||
---
|
||||
title: RevenueCat
|
||||
description: Manage in-app subscriptions and entitlements
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="revenuecat"
|
||||
color="#F25A5A"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[RevenueCat](https://www.revenuecat.com/) is a subscription management platform that enables you to easily set up, manage, and analyze in-app subscriptions for your apps. With RevenueCat, you can handle the complexities of in-app purchases across platforms like iOS, Android, and web—all through a single unified API.
|
||||
|
||||
With RevenueCat, you can:
|
||||
|
||||
- **Manage subscribers**: Track user subscriptions, entitlements, and purchases across all platforms in real time
|
||||
- **Simplify implementation**: Integrate RevenueCat’s SDKs to abstract away App Store and Play Store purchase logic
|
||||
- **Automate entitlement logic**: Define and manage what features users should receive when they purchase or renew
|
||||
- **Analyze revenue**: Access dashboards and analytics to view churn, LTV, revenue, active subscriptions, and more
|
||||
- **Grant or revoke entitlements**: Manually adjust user access (for example, for customer support or promotions)
|
||||
- **Operate globally**: Support purchases, refunds, and promotions worldwide with ease
|
||||
|
||||
In Sim, the RevenueCat integration allows your agents to fetch and manage subscriber data, review and update entitlements, and automate subscription-related workflows. Use RevenueCat to centralize subscription operations for your apps directly within your Sim workspace.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate RevenueCat into the workflow. Manage subscribers, entitlements, offerings, and Google Play subscriptions. Retrieve customer subscription status, grant or revoke promotional entitlements, record purchases, update subscriber attributes, and manage Google Play subscription billing.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `revenuecat_get_customer`
|
||||
|
||||
Retrieve subscriber information by app user ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) |
|
||||
| `appUserId` | string | Yes | The app user ID of the subscriber |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `subscriber` | object | The subscriber object with subscriptions and entitlements |
|
||||
| ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen |
|
||||
| ↳ `original_app_user_id` | string | Original app user ID |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase |
|
||||
| ↳ `management_url` | string | URL for managing the subscriber subscriptions |
|
||||
| ↳ `subscriptions` | object | Map of product identifiers to subscription objects |
|
||||
| ↳ `store_transaction_id` | string | Store transaction identifier |
|
||||
| ↳ `original_transaction_id` | string | Original transaction identifier |
|
||||
| ↳ `purchase_date` | string | ISO 8601 purchase date |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of the original purchase |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `is_sandbox` | boolean | Whether this is a sandbox purchase |
|
||||
| ↳ `unsubscribe_detected_at` | string | ISO 8601 date when unsubscribe was detected |
|
||||
| ↳ `billing_issues_detected_at` | string | ISO 8601 date when billing issues were detected |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `ownership_type` | string | Ownership type \(purchased, family_shared\) |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional, prepaid\) |
|
||||
| ↳ `store` | string | Store the subscription was purchased from \(app_store, play_store, stripe, etc.\) |
|
||||
| ↳ `refunded_at` | string | ISO 8601 date when subscription was refunded |
|
||||
| ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume |
|
||||
| ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) |
|
||||
| ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects |
|
||||
| ↳ `grant_date` | string | ISO 8601 grant date |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `product_identifier` | string | Product identifier |
|
||||
| ↳ `is_active` | boolean | Whether the entitlement is active |
|
||||
| ↳ `will_renew` | boolean | Whether the entitlement will renew |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) |
|
||||
| ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal |
|
||||
| ↳ `store` | string | Store the entitlement was granted from |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects |
|
||||
| `metadata` | object | Subscriber summary metadata |
|
||||
| ↳ `app_user_id` | string | The app user ID |
|
||||
| ↳ `first_seen` | string | ISO 8601 date when the subscriber was first seen |
|
||||
| ↳ `active_entitlements` | number | Number of active entitlements |
|
||||
| ↳ `active_subscriptions` | number | Number of active subscriptions |
|
||||
|
||||
### `revenuecat_delete_customer`
|
||||
|
||||
Permanently delete a subscriber and all associated data
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) |
|
||||
| `appUserId` | string | Yes | The app user ID of the subscriber to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the subscriber was deleted |
|
||||
| `app_user_id` | string | The deleted app user ID |
|
||||
|
||||
### `revenuecat_create_purchase`
|
||||
|
||||
Record a purchase (receipt) for a subscriber via the REST API
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | RevenueCat API key \(public or secret\) |
|
||||
| `appUserId` | string | Yes | The app user ID of the subscriber |
|
||||
| `fetchToken` | string | Yes | The receipt token or purchase token from the store \(App Store receipt, Google Play purchase token, or Stripe subscription ID\) |
|
||||
| `productId` | string | Yes | The product identifier for the purchase |
|
||||
| `price` | number | No | The price of the product in the currency specified |
|
||||
| `currency` | string | No | ISO 4217 currency code \(e.g., USD, EUR\) |
|
||||
| `isRestore` | boolean | No | Whether this is a restore of a previous purchase |
|
||||
| `platform` | string | No | Platform of the purchase \(ios, android, amazon, macos, stripe\). Required for Stripe and Paddle purchases. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `subscriber` | object | The updated subscriber object after recording the purchase |
|
||||
| ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen |
|
||||
| ↳ `original_app_user_id` | string | Original app user ID |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase |
|
||||
| ↳ `management_url` | string | URL for managing the subscriber subscriptions |
|
||||
| ↳ `subscriptions` | object | Map of product identifiers to subscription objects |
|
||||
| ↳ `store_transaction_id` | string | Store transaction identifier |
|
||||
| ↳ `original_transaction_id` | string | Original transaction identifier |
|
||||
| ↳ `purchase_date` | string | ISO 8601 purchase date |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of the original purchase |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `is_sandbox` | boolean | Whether this is a sandbox purchase |
|
||||
| ↳ `unsubscribe_detected_at` | string | ISO 8601 date when unsubscribe was detected |
|
||||
| ↳ `billing_issues_detected_at` | string | ISO 8601 date when billing issues were detected |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `ownership_type` | string | Ownership type \(purchased, family_shared\) |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional, prepaid\) |
|
||||
| ↳ `store` | string | Store the subscription was purchased from \(app_store, play_store, stripe, etc.\) |
|
||||
| ↳ `refunded_at` | string | ISO 8601 date when subscription was refunded |
|
||||
| ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume |
|
||||
| ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) |
|
||||
| ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects |
|
||||
| ↳ `grant_date` | string | ISO 8601 grant date |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `product_identifier` | string | Product identifier |
|
||||
| ↳ `is_active` | boolean | Whether the entitlement is active |
|
||||
| ↳ `will_renew` | boolean | Whether the entitlement will renew |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) |
|
||||
| ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal |
|
||||
| ↳ `store` | string | Store the entitlement was granted from |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects |
|
||||
|
||||
### `revenuecat_grant_entitlement`
|
||||
|
||||
Grant a promotional entitlement to a subscriber
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) |
|
||||
| `appUserId` | string | Yes | The app user ID of the subscriber |
|
||||
| `entitlementIdentifier` | string | Yes | The entitlement identifier to grant |
|
||||
| `duration` | string | Yes | Duration of the entitlement \(daily, three_day, weekly, monthly, two_month, three_month, six_month, yearly, lifetime\) |
|
||||
| `startTimeMs` | number | No | Optional start time in milliseconds since Unix epoch. Set to a past time to achieve custom durations shorter than daily. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `subscriber` | object | The updated subscriber object after granting the entitlement |
|
||||
| ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen |
|
||||
| ↳ `original_app_user_id` | string | Original app user ID |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase |
|
||||
| ↳ `management_url` | string | URL for managing the subscriber subscriptions |
|
||||
| ↳ `subscriptions` | object | Map of product identifiers to subscription objects |
|
||||
| ↳ `store_transaction_id` | string | Store transaction identifier |
|
||||
| ↳ `original_transaction_id` | string | Original transaction identifier |
|
||||
| ↳ `purchase_date` | string | ISO 8601 purchase date |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of the original purchase |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `is_sandbox` | boolean | Whether this is a sandbox purchase |
|
||||
| ↳ `unsubscribe_detected_at` | string | ISO 8601 date when unsubscribe was detected |
|
||||
| ↳ `billing_issues_detected_at` | string | ISO 8601 date when billing issues were detected |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `ownership_type` | string | Ownership type \(purchased, family_shared\) |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional, prepaid\) |
|
||||
| ↳ `store` | string | Store the subscription was purchased from \(app_store, play_store, stripe, etc.\) |
|
||||
| ↳ `refunded_at` | string | ISO 8601 date when subscription was refunded |
|
||||
| ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume |
|
||||
| ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) |
|
||||
| ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects |
|
||||
| ↳ `grant_date` | string | ISO 8601 grant date |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `product_identifier` | string | Product identifier |
|
||||
| ↳ `is_active` | boolean | Whether the entitlement is active |
|
||||
| ↳ `will_renew` | boolean | Whether the entitlement will renew |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) |
|
||||
| ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal |
|
||||
| ↳ `store` | string | Store the entitlement was granted from |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects |
|
||||
|
||||
### `revenuecat_revoke_entitlement`
|
||||
|
||||
Revoke all promotional entitlements for a specific entitlement identifier
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) |
|
||||
| `appUserId` | string | Yes | The app user ID of the subscriber |
|
||||
| `entitlementIdentifier` | string | Yes | The entitlement identifier to revoke |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `subscriber` | object | The updated subscriber object after revoking the entitlement |
|
||||
| ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen |
|
||||
| ↳ `original_app_user_id` | string | Original app user ID |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase |
|
||||
| ↳ `management_url` | string | URL for managing the subscriber subscriptions |
|
||||
| ↳ `subscriptions` | object | Map of product identifiers to subscription objects |
|
||||
| ↳ `store_transaction_id` | string | Store transaction identifier |
|
||||
| ↳ `original_transaction_id` | string | Original transaction identifier |
|
||||
| ↳ `purchase_date` | string | ISO 8601 purchase date |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of the original purchase |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `is_sandbox` | boolean | Whether this is a sandbox purchase |
|
||||
| ↳ `unsubscribe_detected_at` | string | ISO 8601 date when unsubscribe was detected |
|
||||
| ↳ `billing_issues_detected_at` | string | ISO 8601 date when billing issues were detected |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `ownership_type` | string | Ownership type \(purchased, family_shared\) |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional, prepaid\) |
|
||||
| ↳ `store` | string | Store the subscription was purchased from \(app_store, play_store, stripe, etc.\) |
|
||||
| ↳ `refunded_at` | string | ISO 8601 date when subscription was refunded |
|
||||
| ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume |
|
||||
| ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) |
|
||||
| ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects |
|
||||
| ↳ `grant_date` | string | ISO 8601 grant date |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `product_identifier` | string | Product identifier |
|
||||
| ↳ `is_active` | boolean | Whether the entitlement is active |
|
||||
| ↳ `will_renew` | boolean | Whether the entitlement will renew |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) |
|
||||
| ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal |
|
||||
| ↳ `store` | string | Store the entitlement was granted from |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects |
|
||||
|
||||
### `revenuecat_list_offerings`
|
||||
|
||||
List all offerings configured for the project
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | RevenueCat API key |
|
||||
| `appUserId` | string | Yes | An app user ID to retrieve offerings for |
|
||||
| `platform` | string | No | Platform to filter offerings \(ios, android, stripe, etc.\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `current_offering_id` | string | The identifier of the current offering |
|
||||
| `offerings` | array | List of offerings |
|
||||
| ↳ `identifier` | string | Offering identifier |
|
||||
| ↳ `description` | string | Offering description |
|
||||
| ↳ `packages` | array | List of packages in the offering |
|
||||
| ↳ `identifier` | string | Package identifier |
|
||||
| ↳ `platform_product_identifier` | string | Platform-specific product identifier |
|
||||
| `metadata` | object | Offerings metadata |
|
||||
| ↳ `count` | number | Number of offerings returned |
|
||||
| ↳ `current_offering_id` | string | Current offering identifier |
|
||||
|
||||
### `revenuecat_update_subscriber_attributes`
|
||||
|
||||
Update custom subscriber attributes (e.g., $email, $displayName, or custom key-value pairs)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) |
|
||||
| `appUserId` | string | Yes | The app user ID of the subscriber |
|
||||
| `attributes` | json | Yes | JSON object of attributes to set. Each key maps to an object with a "value" field. Example: \{"$email": \{"value": "user@example.com"\}, "$displayName": \{"value": "John"\}\} |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `updated` | boolean | Whether the subscriber attributes were successfully updated |
|
||||
| `app_user_id` | string | The app user ID of the updated subscriber |
|
||||
|
||||
### `revenuecat_defer_google_subscription`
|
||||
|
||||
Defer a Google Play subscription by extending its billing date by a number of days (Google Play only)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) |
|
||||
| `appUserId` | string | Yes | The app user ID of the subscriber |
|
||||
| `productId` | string | Yes | The Google Play product identifier of the subscription to defer \(use the part before the colon for products set up after Feb 2023\) |
|
||||
| `extendByDays` | number | Yes | Number of days to extend the subscription by \(1-365\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `subscriber` | object | The updated subscriber object after deferring the Google subscription |
|
||||
| ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen |
|
||||
| ↳ `original_app_user_id` | string | Original app user ID |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase |
|
||||
| ↳ `management_url` | string | URL for managing the subscriber subscriptions |
|
||||
| ↳ `subscriptions` | object | Map of product identifiers to subscription objects |
|
||||
| ↳ `store_transaction_id` | string | Store transaction identifier |
|
||||
| ↳ `original_transaction_id` | string | Original transaction identifier |
|
||||
| ↳ `purchase_date` | string | ISO 8601 purchase date |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of the original purchase |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `is_sandbox` | boolean | Whether this is a sandbox purchase |
|
||||
| ↳ `unsubscribe_detected_at` | string | ISO 8601 date when unsubscribe was detected |
|
||||
| ↳ `billing_issues_detected_at` | string | ISO 8601 date when billing issues were detected |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `ownership_type` | string | Ownership type \(purchased, family_shared\) |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional, prepaid\) |
|
||||
| ↳ `store` | string | Store the subscription was purchased from \(app_store, play_store, stripe, etc.\) |
|
||||
| ↳ `refunded_at` | string | ISO 8601 date when subscription was refunded |
|
||||
| ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume |
|
||||
| ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) |
|
||||
| ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects |
|
||||
| ↳ `grant_date` | string | ISO 8601 grant date |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `product_identifier` | string | Product identifier |
|
||||
| ↳ `is_active` | boolean | Whether the entitlement is active |
|
||||
| ↳ `will_renew` | boolean | Whether the entitlement will renew |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) |
|
||||
| ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal |
|
||||
| ↳ `store` | string | Store the entitlement was granted from |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects |
|
||||
|
||||
### `revenuecat_refund_google_subscription`
|
||||
|
||||
Refund and optionally revoke a Google Play subscription (Google Play only)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) |
|
||||
| `appUserId` | string | Yes | The app user ID of the subscriber |
|
||||
| `productId` | string | Yes | The Google Play product identifier of the subscription to refund |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `subscriber` | object | The updated subscriber object after refunding the Google subscription |
|
||||
| ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen |
|
||||
| ↳ `original_app_user_id` | string | Original app user ID |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase |
|
||||
| ↳ `management_url` | string | URL for managing the subscriber subscriptions |
|
||||
| ↳ `subscriptions` | object | Map of product identifiers to subscription objects |
|
||||
| ↳ `store_transaction_id` | string | Store transaction identifier |
|
||||
| ↳ `original_transaction_id` | string | Original transaction identifier |
|
||||
| ↳ `purchase_date` | string | ISO 8601 purchase date |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of the original purchase |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `is_sandbox` | boolean | Whether this is a sandbox purchase |
|
||||
| ↳ `unsubscribe_detected_at` | string | ISO 8601 date when unsubscribe was detected |
|
||||
| ↳ `billing_issues_detected_at` | string | ISO 8601 date when billing issues were detected |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `ownership_type` | string | Ownership type \(purchased, family_shared\) |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional, prepaid\) |
|
||||
| ↳ `store` | string | Store the subscription was purchased from \(app_store, play_store, stripe, etc.\) |
|
||||
| ↳ `refunded_at` | string | ISO 8601 date when subscription was refunded |
|
||||
| ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume |
|
||||
| ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) |
|
||||
| ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects |
|
||||
| ↳ `grant_date` | string | ISO 8601 grant date |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `product_identifier` | string | Product identifier |
|
||||
| ↳ `is_active` | boolean | Whether the entitlement is active |
|
||||
| ↳ `will_renew` | boolean | Whether the entitlement will renew |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) |
|
||||
| ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal |
|
||||
| ↳ `store` | string | Store the entitlement was granted from |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects |
|
||||
|
||||
### `revenuecat_revoke_google_subscription`
|
||||
|
||||
Immediately revoke access to a Google Play subscription and issue a refund (Google Play only)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) |
|
||||
| `appUserId` | string | Yes | The app user ID of the subscriber |
|
||||
| `productId` | string | Yes | The Google Play product identifier of the subscription to revoke |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `subscriber` | object | The updated subscriber object after revoking the Google subscription |
|
||||
| ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen |
|
||||
| ↳ `original_app_user_id` | string | Original app user ID |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase |
|
||||
| ↳ `management_url` | string | URL for managing the subscriber subscriptions |
|
||||
| ↳ `subscriptions` | object | Map of product identifiers to subscription objects |
|
||||
| ↳ `store_transaction_id` | string | Store transaction identifier |
|
||||
| ↳ `original_transaction_id` | string | Original transaction identifier |
|
||||
| ↳ `purchase_date` | string | ISO 8601 purchase date |
|
||||
| ↳ `original_purchase_date` | string | ISO 8601 date of the original purchase |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `is_sandbox` | boolean | Whether this is a sandbox purchase |
|
||||
| ↳ `unsubscribe_detected_at` | string | ISO 8601 date when unsubscribe was detected |
|
||||
| ↳ `billing_issues_detected_at` | string | ISO 8601 date when billing issues were detected |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `ownership_type` | string | Ownership type \(purchased, family_shared\) |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional, prepaid\) |
|
||||
| ↳ `store` | string | Store the subscription was purchased from \(app_store, play_store, stripe, etc.\) |
|
||||
| ↳ `refunded_at` | string | ISO 8601 date when subscription was refunded |
|
||||
| ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume |
|
||||
| ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) |
|
||||
| ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects |
|
||||
| ↳ `grant_date` | string | ISO 8601 grant date |
|
||||
| ↳ `expires_date` | string | ISO 8601 expiration date |
|
||||
| ↳ `product_identifier` | string | Product identifier |
|
||||
| ↳ `is_active` | boolean | Whether the entitlement is active |
|
||||
| ↳ `will_renew` | boolean | Whether the entitlement will renew |
|
||||
| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) |
|
||||
| ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal |
|
||||
| ↳ `store` | string | Store the entitlement was granted from |
|
||||
| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date |
|
||||
| ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects |
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Slack
|
||||
description: Send, update, delete messages, add reactions in Slack or trigger workflows from Slack events
|
||||
description: Send, update, delete messages, send ephemeral messages, add reactions in Slack or trigger workflows from Slack events
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -59,7 +59,7 @@ If you encounter issues with the Slack integration, contact us at [help@sim.ai](
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Slack into the workflow. Can send, update, and delete messages, create canvases, read messages, and add reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.
|
||||
Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, create canvases, read messages, and add reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
|
||||
| `dmUserId` | string | No | Slack user ID for direct messages \(e.g., U1234567890\) |
|
||||
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
|
||||
| `threadTs` | string | No | Thread timestamp to reply to \(creates thread reply\) |
|
||||
| `blocks` | json | No | Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text. |
|
||||
| `files` | file[] | No | Files to attach to the message |
|
||||
|
||||
#### Output
|
||||
@@ -146,6 +147,29 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
|
||||
| `fileCount` | number | Number of files uploaded \(when files are attached\) |
|
||||
| `files` | file[] | Files attached to the message |
|
||||
|
||||
### `slack_ephemeral_message`
|
||||
|
||||
Send an ephemeral message visible only to a specific user in a channel. Optionally reply in a thread. The message does not persist across sessions.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | Yes | Slack channel ID \(e.g., C1234567890\) |
|
||||
| `user` | string | Yes | User ID who will see the ephemeral message \(e.g., U1234567890\). Must be a member of the channel. |
|
||||
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
|
||||
| `threadTs` | string | No | Thread timestamp to reply in. When provided, the ephemeral message appears as a thread reply. |
|
||||
| `blocks` | json | No | Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageTs` | string | Timestamp of the ephemeral message \(cannot be used with chat.update\) |
|
||||
| `channel` | string | Channel ID where the ephemeral message was sent |
|
||||
|
||||
### `slack_canvas`
|
||||
|
||||
Create and share Slack canvases in channels. Canvases are collaborative documents within Slack.
|
||||
@@ -682,6 +706,7 @@ Update a message previously sent by the bot in Slack
|
||||
| `channel` | string | Yes | Channel ID where the message was posted \(e.g., C1234567890\) |
|
||||
| `timestamp` | string | Yes | Timestamp of the message to update \(e.g., 1405894322.002768\) |
|
||||
| `text` | string | Yes | New message text \(supports Slack mrkdwn formatting\) |
|
||||
| `blocks` | json | No | Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text. |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
353
apps/docs/content/docs/en/tools/table.mdx
Normal file
353
apps/docs/content/docs/en/tools/table.mdx
Normal file
@@ -0,0 +1,353 @@
|
||||
---
|
||||
title: Table
|
||||
description: User-defined data tables for storing and querying structured data
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="table"
|
||||
color="#10B981"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
Tables allow you to create and manage custom data tables directly within Sim. Store, query, and manipulate structured data within your workflows without needing external database integrations.
|
||||
|
||||
**Why Use Tables?**
|
||||
- **No external setup**: Create tables instantly without configuring external databases
|
||||
- **Workflow-native**: Data persists across workflow executions and is accessible from any workflow in your workspace
|
||||
- **Flexible schema**: Define columns with types (string, number, boolean, date, json) and constraints (required, unique)
|
||||
- **Powerful querying**: Filter, sort, and paginate data using MongoDB-style operators
|
||||
- **Agent-friendly**: Tables can be used as tools by AI agents for dynamic data storage and retrieval
|
||||
|
||||
**Key Features:**
|
||||
- Create tables with custom schemas
|
||||
- Insert, update, upsert, and delete rows
|
||||
- Query with filters and sorting
|
||||
- Batch operations for bulk inserts
|
||||
- Bulk updates and deletes by filter
|
||||
- Up to 10,000 rows per table, 100 tables per workspace
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Creating Tables
|
||||
|
||||
Tables are created from the **Tables** section in the sidebar. Each table requires:
|
||||
- **Name**: Alphanumeric with underscores (e.g., `customer_leads`)
|
||||
- **Description**: Optional description of the table's purpose
|
||||
- **Schema**: Define columns with name, type, and optional constraints
|
||||
|
||||
### Column Types
|
||||
|
||||
| Type | Description | Example Values |
|
||||
|------|-------------|----------------|
|
||||
| `string` | Text data | `"John Doe"`, `"active"` |
|
||||
| `number` | Numeric data | `42`, `99.99` |
|
||||
| `boolean` | True/false values | `true`, `false` |
|
||||
| `date` | Date/time values | `"2024-01-15T10:30:00Z"` |
|
||||
| `json` | Complex nested data | `{"address": {"city": "NYC"}}` |
|
||||
|
||||
### Column Constraints
|
||||
|
||||
- **Required**: Column must have a value (cannot be null)
|
||||
- **Unique**: Values must be unique across all rows (enables upsert matching)
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Create and manage custom data tables. Store, query, and manipulate structured data within workflows.
|
||||
|
||||
## Tools
|
||||
|
||||
### `table_query_rows`
|
||||
|
||||
Query rows from a table with filtering, sorting, and pagination
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tableId` | string | Yes | Table ID |
|
||||
| `filter` | object | No | Filter conditions using MongoDB-style operators |
|
||||
| `sort` | object | No | Sort order as \{column: "asc"\|"desc"\} |
|
||||
| `limit` | number | No | Maximum rows to return \(default: 100, max: 1000\) |
|
||||
| `offset` | number | No | Number of rows to skip \(default: 0\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether query succeeded |
|
||||
| `rows` | array | Query result rows |
|
||||
| `rowCount` | number | Number of rows returned |
|
||||
| `totalCount` | number | Total rows matching filter |
|
||||
| `limit` | number | Limit used in query |
|
||||
| `offset` | number | Offset used in query |
|
||||
|
||||
### `table_insert_row`
|
||||
|
||||
Insert a new row into a table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tableId` | string | Yes | Table ID |
|
||||
| `data` | object | Yes | Row data as JSON object matching the table schema |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether row was inserted |
|
||||
| `row` | object | Inserted row data including generated ID |
|
||||
| `message` | string | Status message |
|
||||
|
||||
### `table_upsert_row`
|
||||
|
||||
Insert or update a row based on unique column constraints. If a row with matching unique field exists, update it; otherwise insert a new row.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tableId` | string | Yes | Table ID |
|
||||
| `data` | object | Yes | Row data to insert or update |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether row was upserted |
|
||||
| `row` | object | Upserted row data |
|
||||
| `operation` | string | Operation performed: "insert" or "update" |
|
||||
| `message` | string | Status message |
|
||||
|
||||
### `table_batch_insert_rows`
|
||||
|
||||
Insert multiple rows at once (up to 1000 rows per batch)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tableId` | string | Yes | Table ID |
|
||||
| `rows` | array | Yes | Array of row data objects to insert |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether batch insert succeeded |
|
||||
| `rows` | array | Array of inserted rows with IDs |
|
||||
| `insertedCount` | number | Number of rows inserted |
|
||||
| `message` | string | Status message |
|
||||
|
||||
### `table_update_row`
|
||||
|
||||
Update a specific row by its ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tableId` | string | Yes | Table ID |
|
||||
| `rowId` | string | Yes | Row ID to update |
|
||||
| `data` | object | Yes | Data to update \(partial update supported\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether row was updated |
|
||||
| `row` | object | Updated row data |
|
||||
| `message` | string | Status message |
|
||||
|
||||
### `table_update_rows_by_filter`
|
||||
|
||||
Update multiple rows matching a filter condition
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tableId` | string | Yes | Table ID |
|
||||
| `filter` | object | Yes | Filter to match rows for update |
|
||||
| `data` | object | Yes | Data to apply to matching rows |
|
||||
| `limit` | number | No | Maximum rows to update \(default: 1000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether update succeeded |
|
||||
| `updatedCount` | number | Number of rows updated |
|
||||
| `updatedRowIds` | array | IDs of updated rows |
|
||||
| `message` | string | Status message |
|
||||
|
||||
### `table_delete_row`
|
||||
|
||||
Delete a specific row by its ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tableId` | string | Yes | Table ID |
|
||||
| `rowId` | string | Yes | Row ID to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether row was deleted |
|
||||
| `deletedCount` | number | Number of rows deleted \(1 or 0\) |
|
||||
| `message` | string | Status message |
|
||||
|
||||
### `table_delete_rows_by_filter`
|
||||
|
||||
Delete multiple rows matching a filter condition
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tableId` | string | Yes | Table ID |
|
||||
| `filter` | object | Yes | Filter to match rows for deletion |
|
||||
| `limit` | number | No | Maximum rows to delete \(default: 1000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether delete succeeded |
|
||||
| `deletedCount` | number | Number of rows deleted |
|
||||
| `deletedRowIds` | array | IDs of deleted rows |
|
||||
| `message` | string | Status message |
|
||||
|
||||
### `table_get_row`
|
||||
|
||||
Get a single row by its ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tableId` | string | Yes | Table ID |
|
||||
| `rowId` | string | Yes | Row ID to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether row was found |
|
||||
| `row` | object | Row data |
|
||||
| `message` | string | Status message |
|
||||
|
||||
### `table_get_schema`
|
||||
|
||||
Get the schema definition for a table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `tableId` | string | Yes | Table ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether schema was retrieved |
|
||||
| `name` | string | Table name |
|
||||
| `columns` | array | Array of column definitions |
|
||||
| `message` | string | Status message |
|
||||
|
||||
## Filter Operators
|
||||
|
||||
Filters use MongoDB-style operators for flexible querying:
|
||||
|
||||
| Operator | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `$eq` | Equals | `{"status": {"$eq": "active"}}` or `{"status": "active"}` |
|
||||
| `$ne` | Not equals | `{"status": {"$ne": "deleted"}}` |
|
||||
| `$gt` | Greater than | `{"age": {"$gt": 18}}` |
|
||||
| `$gte` | Greater than or equal | `{"score": {"$gte": 80}}` |
|
||||
| `$lt` | Less than | `{"price": {"$lt": 100}}` |
|
||||
| `$lte` | Less than or equal | `{"quantity": {"$lte": 10}}` |
|
||||
| `$in` | In array | `{"status": {"$in": ["active", "pending"]}}` |
|
||||
| `$nin` | Not in array | `{"type": {"$nin": ["spam", "blocked"]}}` |
|
||||
| `$contains` | String contains | `{"email": {"$contains": "@gmail.com"}}` |
|
||||
|
||||
### Combining Filters
|
||||
|
||||
Multiple field conditions are combined with AND logic:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "active",
|
||||
"age": {"$gte": 18}
|
||||
}
|
||||
```
|
||||
|
||||
Use `$or` for OR logic:
|
||||
|
||||
```json
|
||||
{
|
||||
"$or": [
|
||||
{"status": "active"},
|
||||
{"status": "pending"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Sort Specification
|
||||
|
||||
Specify sort order with column names and direction:
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": "desc"
|
||||
}
|
||||
```
|
||||
|
||||
Multi-column sorting:
|
||||
|
||||
```json
|
||||
{
|
||||
"priority": "desc",
|
||||
"name": "asc"
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in Columns
|
||||
|
||||
Every row automatically includes:
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | string | Unique row identifier |
|
||||
| `createdAt` | date | When the row was created |
|
||||
| `updatedAt` | date | When the row was last modified |
|
||||
|
||||
These can be used in filters and sorting.
|
||||
|
||||
## Limits
|
||||
|
||||
| Resource | Limit |
|
||||
|----------|-------|
|
||||
| Tables per workspace | 100 |
|
||||
| Rows per table | 10,000 |
|
||||
| Columns per table | 50 |
|
||||
| Max row size | 100KB |
|
||||
| String value length | 10,000 characters |
|
||||
| Query limit | 1,000 rows |
|
||||
| Batch insert size | 1,000 rows |
|
||||
| Bulk update/delete | 1,000 rows |
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `blocks`
|
||||
- Type: `table`
|
||||
- Tables are scoped to workspaces and accessible from any workflow within that workspace
|
||||
- Data persists across workflow executions
|
||||
- Use unique constraints to enable upsert functionality
|
||||
- The visual filter/sort builder provides an easy way to construct queries without writing JSON
|
||||
357
apps/docs/content/docs/en/tools/upstash.mdx
Normal file
357
apps/docs/content/docs/en/tools/upstash.mdx
Normal file
@@ -0,0 +1,357 @@
|
||||
---
|
||||
title: Upstash
|
||||
description: Serverless Redis with Upstash
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="upstash"
|
||||
color="#181C1E"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Upstash](https://upstash.com/) is a serverless data platform designed for modern applications that need fast, simple, and scalable data storage with minimal setup. Upstash specializes in providing Redis and Kafka as fully managed, pay-per-request cloud services, making it a popular choice for developers building serverless, edge, and event-driven architectures.
|
||||
|
||||
With Upstash Redis, you can:
|
||||
|
||||
- **Store and retrieve data instantly**: Read and write key-value pairs, hashes, lists, sets, and more—all over a high-performance REST API.
|
||||
- **Scale serverlessly**: No infrastructure to manage. Upstash automatically scales with your app and charges only for what you use.
|
||||
- **Access globally**: Deploy near your users with multi-region support and global distribution.
|
||||
- **Integrate easily**: Use Upstash’s REST API in serverless functions, edge workers, Next.js, Vercel, Cloudflare Workers, and more.
|
||||
- **Automate with scripts**: Run Lua scripts for advanced transactions and automation.
|
||||
- **Ensure security**: Protect your data with built-in authentication and TLS encryption.
|
||||
|
||||
In Sim, the Upstash integration empowers your agents and workflows to read, write, and manage data in Upstash Redis using simple, unified commands—perfect for building scalable automations, caching results, managing queues, and more, all without dealing with server management.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect to Upstash Redis to perform key-value, hash, list, and utility operations via the REST API.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `upstash_redis_get`
|
||||
|
||||
Get the value of a key from Upstash Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The key to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was retrieved |
|
||||
| `value` | json | The value of the key \(string\), or null if not found |
|
||||
|
||||
### `upstash_redis_set`
|
||||
|
||||
Set the value of a key in Upstash Redis with an optional expiration time in seconds.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The key to set |
|
||||
| `value` | string | Yes | The value to store |
|
||||
| `ex` | number | No | Expiration time in seconds \(optional\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was set |
|
||||
| `result` | string | The result of the SET operation \(typically "OK"\) |
|
||||
|
||||
### `upstash_redis_delete`
|
||||
|
||||
Delete a key from Upstash Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The key to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was deleted |
|
||||
| `deletedCount` | number | Number of keys deleted \(0 if key did not exist, 1 if deleted\) |
|
||||
|
||||
### `upstash_redis_keys`
|
||||
|
||||
List keys matching a pattern in Upstash Redis. Defaults to listing all keys (*).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `pattern` | string | No | Pattern to match keys \(e.g., "user:*"\). Defaults to "*" for all keys. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `pattern` | string | The pattern used to match keys |
|
||||
| `keys` | array | List of keys matching the pattern |
|
||||
| `count` | number | Number of keys found |
|
||||
|
||||
### `upstash_redis_command`
|
||||
|
||||
Execute an arbitrary Redis command against Upstash Redis. Pass the full command as a JSON array (e.g., [
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `command` | string | Yes | Redis command as a JSON array \(e.g., \["HSET", "myhash", "field1", "value1"\]\) or a simple command string \(e.g., "PING"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `command` | string | The command that was executed |
|
||||
| `result` | json | The result of the Redis command |
|
||||
|
||||
### `upstash_redis_hset`
|
||||
|
||||
Set a field in a hash stored at a key in Upstash Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The hash key |
|
||||
| `field` | string | Yes | The field name within the hash |
|
||||
| `value` | string | Yes | The value to store in the hash field |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The hash key |
|
||||
| `field` | string | The field that was set |
|
||||
| `result` | number | Number of new fields added \(0 if field was updated, 1 if new\) |
|
||||
|
||||
### `upstash_redis_hget`
|
||||
|
||||
Get the value of a field in a hash stored at a key in Upstash Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The hash key |
|
||||
| `field` | string | Yes | The field name to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The hash key |
|
||||
| `field` | string | The field that was retrieved |
|
||||
| `value` | json | The value of the hash field \(string\), or null if not found |
|
||||
|
||||
### `upstash_redis_hgetall`
|
||||
|
||||
Get all fields and values of a hash stored at a key in Upstash Redis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The hash key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The hash key |
|
||||
| `fields` | object | All field-value pairs in the hash, keyed by field name |
|
||||
| `fieldCount` | number | Number of fields in the hash |
|
||||
|
||||
### `upstash_redis_incr`
|
||||
|
||||
Atomically increment the integer value of a key by one in Upstash Redis. If the key does not exist, it is set to 0 before incrementing.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The key to increment |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was incremented |
|
||||
| `value` | number | The new value after incrementing |
|
||||
|
||||
### `upstash_redis_expire`
|
||||
|
||||
Set a timeout on a key in Upstash Redis. After the timeout, the key is deleted.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The key to set expiration on |
|
||||
| `seconds` | number | Yes | Timeout in seconds |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that expiration was set on |
|
||||
| `result` | number | 1 if the timeout was set, 0 if the key does not exist |
|
||||
|
||||
### `upstash_redis_ttl`
|
||||
|
||||
Get the remaining time to live of a key in Upstash Redis. Returns -1 if the key has no expiration, -2 if the key does not exist.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The key to check TTL for |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key checked |
|
||||
| `ttl` | number | Remaining TTL in seconds. Positive integer if the key has a TTL set, -1 if the key exists with no expiration, -2 if the key does not exist. |
|
||||
|
||||
### `upstash_redis_lpush`
|
||||
|
||||
Prepend a value to the beginning of a list in Upstash Redis. Creates the list if it does not exist.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The list key |
|
||||
| `value` | string | Yes | The value to prepend to the list |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The list key |
|
||||
| `length` | number | The length of the list after the push |
|
||||
|
||||
### `upstash_redis_lrange`
|
||||
|
||||
Get a range of elements from a list in Upstash Redis. Use 0 and -1 for start and stop to get all elements.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The list key |
|
||||
| `start` | number | Yes | Start index \(0-based, negative values count from end\) |
|
||||
| `stop` | number | Yes | Stop index \(inclusive, -1 for last element\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The list key |
|
||||
| `values` | array | List of elements in the specified range |
|
||||
| `count` | number | Number of elements returned |
|
||||
|
||||
### `upstash_redis_exists`
|
||||
|
||||
Check if a key exists in Upstash Redis. Returns true if the key exists, false otherwise.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The key to check |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was checked |
|
||||
| `exists` | boolean | Whether the key exists \(true\) or not \(false\) |
|
||||
|
||||
### `upstash_redis_setnx`
|
||||
|
||||
Set the value of a key only if it does not already exist. Returns true if the key was set, false if it already existed.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The key to set |
|
||||
| `value` | string | Yes | The value to store if the key does not exist |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was attempted to set |
|
||||
| `wasSet` | boolean | Whether the key was set \(true\) or already existed \(false\) |
|
||||
|
||||
### `upstash_redis_incrby`
|
||||
|
||||
Increment the integer value of a key by a given amount. Use a negative value to decrement. If the key does not exist, it is set to 0 before the operation.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `restUrl` | string | Yes | Upstash Redis REST URL |
|
||||
| `restToken` | string | Yes | Upstash Redis REST Token |
|
||||
| `key` | string | Yes | The key to increment |
|
||||
| `increment` | number | Yes | Amount to increment by \(use negative value to decrement\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The key that was incremented |
|
||||
| `value` | number | The new value after incrementing |
|
||||
|
||||
|
||||
1391
apps/docs/content/docs/en/tools/vercel.mdx
Normal file
1391
apps/docs/content/docs/en/tools/vercel.mdx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: Environment Variables
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Environment variables provide a secure way to manage configuration values and secrets across your workflows, including API keys and other sensitive data that your workflows need to access. They keep secrets out of your workflow definitions while making them available during execution.
|
||||
|
||||
## Variable Types
|
||||
|
||||
Environment variables in Sim work at two levels:
|
||||
|
||||
- **Personal Environment Variables**: Private to your account, only you can see and use them
|
||||
- **Workspace Environment Variables**: Shared across the entire workspace, available to all team members
|
||||
|
||||
<Callout type="info">
|
||||
Workspace environment variables take precedence over personal ones when there's a naming conflict.
|
||||
</Callout>
|
||||
|
||||
## Setting up Environment Variables
|
||||
|
||||
Navigate to Settings to configure your environment variables:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="Environment variables modal for creating new variables"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
From your workspace settings, you can create and manage both personal and workspace-level environment variables. Personal variables are private to your account, while workspace variables are shared with all team members.
|
||||
|
||||
### Making Variables Workspace-Scoped
|
||||
|
||||
Use the workspace scope toggle to make variables available to your entire team:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="Toggle workspace scope for environment variables"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
When you enable workspace scope, the variable becomes available to all workspace members and can be used in any workflow within that workspace.
|
||||
|
||||
### Workspace Variables View
|
||||
|
||||
Once you have workspace-scoped variables, they appear in your environment variables list:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="Workspace-scoped variables in the environment variables list"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Using Variables in Workflows
|
||||
|
||||
To reference environment variables in your workflows, use the `{{}}` notation. When you type `{{` in any input field, a dropdown will appear showing both your personal and workspace-level environment variables. Simply select the variable you want to use.
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="Using environment variables with double brace notation"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## How Variables are Resolved
|
||||
|
||||
**Workspace variables always take precedence** over personal variables, regardless of who runs the workflow.
|
||||
|
||||
When no workspace variable exists for a key, personal variables are used:
|
||||
- **Manual runs (UI)**: Your personal variables
|
||||
- **Automated runs (API, webhook, schedule, deployed chat)**: Workflow owner's personal variables
|
||||
|
||||
<Callout type="info">
|
||||
Personal variables are best for testing. Use workspace variables for production workflows.
|
||||
</Callout>
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### For Sensitive Data
|
||||
- Store API keys, tokens, and passwords as environment variables instead of hardcoding them
|
||||
- Use workspace variables for shared resources that multiple team members need
|
||||
- Keep personal credentials in personal variables
|
||||
|
||||
### Variable Naming
|
||||
- Use descriptive names: `DATABASE_URL` instead of `DB`
|
||||
- Follow consistent naming conventions across your team
|
||||
- Consider prefixes to avoid conflicts: `PROD_API_KEY`, `DEV_API_KEY`
|
||||
|
||||
### Access Control
|
||||
- Workspace environment variables respect workspace permissions
|
||||
- Only users with write access or higher can create/modify workspace variables
|
||||
- Personal variables are always private to the individual user
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: Variables de entorno
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Las variables de entorno proporcionan una forma segura de gestionar valores de configuración y secretos en tus flujos de trabajo, incluyendo claves API y otros datos sensibles que tus flujos de trabajo necesitan acceder. Mantienen los secretos fuera de las definiciones de tu flujo de trabajo mientras los hacen disponibles durante la ejecución.
|
||||
|
||||
## Tipos de variables
|
||||
|
||||
Las variables de entorno en Sim funcionan en dos niveles:
|
||||
|
||||
- **Variables de entorno personales**: Privadas para tu cuenta, solo tú puedes verlas y usarlas
|
||||
- **Variables de entorno del espacio de trabajo**: Compartidas en todo el espacio de trabajo, disponibles para todos los miembros del equipo
|
||||
|
||||
<Callout type="info">
|
||||
Las variables de entorno del espacio de trabajo tienen prioridad sobre las personales cuando hay un conflicto de nombres.
|
||||
</Callout>
|
||||
|
||||
## Configuración de variables de entorno
|
||||
|
||||
Navega a Configuración para configurar tus variables de entorno:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="Modal de variables de entorno para crear nuevas variables"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
Desde la configuración de tu espacio de trabajo, puedes crear y gestionar variables de entorno tanto personales como a nivel de espacio de trabajo. Las variables personales son privadas para tu cuenta, mientras que las variables del espacio de trabajo se comparten con todos los miembros del equipo.
|
||||
|
||||
### Hacer variables con ámbito de espacio de trabajo
|
||||
|
||||
Usa el interruptor de ámbito del espacio de trabajo para hacer que las variables estén disponibles para todo tu equipo:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="Interruptor de ámbito del espacio de trabajo para variables de entorno"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
Cuando habilitas el ámbito del espacio de trabajo, la variable se vuelve disponible para todos los miembros del espacio de trabajo y puede ser utilizada en cualquier flujo de trabajo dentro de ese espacio de trabajo.
|
||||
|
||||
### Vista de variables del espacio de trabajo
|
||||
|
||||
Una vez que tienes variables con ámbito de espacio de trabajo, aparecen en tu lista de variables de entorno:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="Variables con ámbito de espacio de trabajo en la lista de variables de entorno"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Uso de variables en flujos de trabajo
|
||||
|
||||
Para hacer referencia a variables de entorno en tus flujos de trabajo, utiliza la notación `{{}}`. Cuando escribas `{{` en cualquier campo de entrada, aparecerá un menú desplegable mostrando tanto tus variables de entorno personales como las del espacio de trabajo. Simplemente selecciona la variable que deseas utilizar.
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="Uso de variables de entorno con notación de doble llave"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Cómo se resuelven las variables
|
||||
|
||||
**Las variables del espacio de trabajo siempre tienen prioridad** sobre las variables personales, independientemente de quién ejecute el flujo de trabajo.
|
||||
|
||||
Cuando no existe una variable de espacio de trabajo para una clave, se utilizan las variables personales:
|
||||
- **Ejecuciones manuales (UI)**: Tus variables personales
|
||||
- **Ejecuciones automatizadas (API, webhook, programación, chat implementado)**: Variables personales del propietario del flujo de trabajo
|
||||
|
||||
<Callout type="info">
|
||||
Las variables personales son mejores para pruebas. Usa variables de espacio de trabajo para flujos de trabajo de producción.
|
||||
</Callout>
|
||||
|
||||
## Mejores prácticas de seguridad
|
||||
|
||||
### Para datos sensibles
|
||||
- Almacena claves API, tokens y contraseñas como variables de entorno en lugar de codificarlos directamente
|
||||
- Usa variables de espacio de trabajo para recursos compartidos que varios miembros del equipo necesitan
|
||||
- Mantén las credenciales personales en variables personales
|
||||
|
||||
### Nomenclatura de variables
|
||||
- Usa nombres descriptivos: `DATABASE_URL` en lugar de `DB`
|
||||
- Sigue convenciones de nomenclatura consistentes en todo tu equipo
|
||||
- Considera usar prefijos para evitar conflictos: `PROD_API_KEY`, `DEV_API_KEY`
|
||||
|
||||
### Control de acceso
|
||||
- Las variables de entorno del espacio de trabajo respetan los permisos del espacio de trabajo
|
||||
- Solo los usuarios con acceso de escritura o superior pueden crear/modificar variables del espacio de trabajo
|
||||
- Las variables personales siempre son privadas para el usuario individual
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: Variables d'environnement
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Les variables d'environnement offrent un moyen sécurisé de gérer les valeurs de configuration et les secrets dans vos workflows, y compris les clés API et autres données sensibles dont vos workflows ont besoin. Elles gardent les secrets en dehors de vos définitions de workflow tout en les rendant disponibles pendant l'exécution.
|
||||
|
||||
## Types de variables
|
||||
|
||||
Les variables d'environnement dans Sim fonctionnent à deux niveaux :
|
||||
|
||||
- **Variables d'environnement personnelles** : privées à votre compte, vous seul pouvez les voir et les utiliser
|
||||
- **Variables d'environnement d'espace de travail** : partagées dans tout l'espace de travail, disponibles pour tous les membres de l'équipe
|
||||
|
||||
<Callout type="info">
|
||||
Les variables d'environnement d'espace de travail ont priorité sur les variables personnelles en cas de conflit de noms.
|
||||
</Callout>
|
||||
|
||||
## Configuration des variables d'environnement
|
||||
|
||||
Accédez aux Paramètres pour configurer vos variables d'environnement :
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="Fenêtre modale de variables d'environnement pour créer de nouvelles variables"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
Depuis les paramètres de votre espace de travail, vous pouvez créer et gérer des variables d'environnement personnelles et au niveau de l'espace de travail. Les variables personnelles sont privées à votre compte, tandis que les variables d'espace de travail sont partagées avec tous les membres de l'équipe.
|
||||
|
||||
### Définir des variables au niveau de l'espace de travail
|
||||
|
||||
Utilisez le bouton de portée d'espace de travail pour rendre les variables disponibles à toute votre équipe :
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="Activer la portée d'espace de travail pour les variables d'environnement"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
Lorsque vous activez la portée d'espace de travail, la variable devient disponible pour tous les membres de l'espace de travail et peut être utilisée dans n'importe quel workflow au sein de cet espace de travail.
|
||||
|
||||
### Vue des variables d'espace de travail
|
||||
|
||||
Une fois que vous avez des variables à portée d'espace de travail, elles apparaissent dans votre liste de variables d'environnement :
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="Variables à portée d'espace de travail dans la liste des variables d'environnement"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Utilisation des variables dans les workflows
|
||||
|
||||
Pour référencer des variables d'environnement dans vos workflows, utilisez la notation `{{}}`. Lorsque vous tapez `{{` dans n'importe quel champ de saisie, un menu déroulant apparaîtra affichant à la fois vos variables d'environnement personnelles et celles au niveau de l'espace de travail. Sélectionnez simplement la variable que vous souhaitez utiliser.
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="Utilisation des variables d'environnement avec la notation à double accolade"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## Comment les variables sont résolues
|
||||
|
||||
**Les variables d'espace de travail ont toujours la priorité** sur les variables personnelles, quel que soit l'utilisateur qui exécute le flux de travail.
|
||||
|
||||
Lorsqu'aucune variable d'espace de travail n'existe pour une clé, les variables personnelles sont utilisées :
|
||||
- **Exécutions manuelles (UI)** : Vos variables personnelles
|
||||
- **Exécutions automatisées (API, webhook, planification, chat déployé)** : Variables personnelles du propriétaire du flux de travail
|
||||
|
||||
<Callout type="info">
|
||||
Les variables personnelles sont idéales pour les tests. Utilisez les variables d'espace de travail pour les flux de travail en production.
|
||||
</Callout>
|
||||
|
||||
## Bonnes pratiques de sécurité
|
||||
|
||||
### Pour les données sensibles
|
||||
- Stockez les clés API, les jetons et les mots de passe comme variables d'environnement au lieu de les coder en dur
|
||||
- Utilisez des variables d'espace de travail pour les ressources partagées dont plusieurs membres de l'équipe ont besoin
|
||||
- Conservez vos identifiants personnels dans des variables personnelles
|
||||
|
||||
### Nommage des variables
|
||||
- Utilisez des noms descriptifs : `DATABASE_URL` au lieu de `DB`
|
||||
- Suivez des conventions de nommage cohérentes au sein de votre équipe
|
||||
- Envisagez des préfixes pour éviter les conflits : `PROD_API_KEY`, `DEV_API_KEY`
|
||||
|
||||
### Contrôle d'accès
|
||||
- Les variables d'environnement de l'espace de travail respectent les permissions de l'espace de travail
|
||||
- Seuls les utilisateurs disposant d'un accès en écriture ou supérieur peuvent créer/modifier les variables d'espace de travail
|
||||
- Les variables personnelles sont toujours privées pour l'utilisateur individuel
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: 環境変数
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
環境変数は、APIキーやワークフローがアクセスする必要のあるその他の機密データなど、ワークフロー全体で設定値や機密情報を安全に管理する方法を提供します。これにより、実行中にそれらを利用可能にしながら、ワークフロー定義から機密情報を切り離すことができます。
|
||||
|
||||
## 変数タイプ
|
||||
|
||||
Simの環境変数は2つのレベルで機能します:
|
||||
|
||||
- **個人環境変数**:あなたのアカウントに限定され、あなただけが閲覧・使用できます
|
||||
- **ワークスペース環境変数**:ワークスペース全体で共有され、すべてのチームメンバーが利用できます
|
||||
|
||||
<Callout type="info">
|
||||
名前の競合がある場合、ワークスペース環境変数は個人環境変数よりも優先されます。
|
||||
</Callout>
|
||||
|
||||
## 環境変数の設定
|
||||
|
||||
設定に移動して環境変数を構成します:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="新しい変数を作成するための環境変数モーダル"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
ワークスペース設定から、個人レベルとワークスペースレベルの両方の環境変数を作成・管理できます。個人変数はあなたのアカウントに限定されますが、ワークスペース変数はすべてのチームメンバーと共有されます。
|
||||
|
||||
### 変数をワークスペーススコープにする
|
||||
|
||||
ワークスペーススコープトグルを使用して、変数をチーム全体で利用可能にします:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="環境変数のワークスペーススコープを切り替えるトグル"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
ワークスペーススコープを有効にすると、その変数はすべてのワークスペースメンバーが利用でき、そのワークスペース内のあらゆるワークフローで使用できるようになります。
|
||||
|
||||
### ワークスペース変数ビュー
|
||||
|
||||
ワークスペーススコープの変数を作成すると、環境変数リストに表示されます:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="環境変数リスト内のワークスペーススコープ変数"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## ワークフローでの変数の使用
|
||||
|
||||
ワークフローで環境変数を参照するには、`{{}}`表記を使用します。任意の入力フィールドで`{{`と入力すると、個人用とワークスペースレベルの両方の環境変数を表示するドロップダウンが表示されます。使用したい変数を選択するだけです。
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="二重括弧表記を使用した環境変数の使用方法"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## 変数の解決方法
|
||||
|
||||
**ワークスペース変数は常に優先されます**。誰がワークフローを実行するかに関わらず、個人変数よりも優先されます。
|
||||
|
||||
キーに対するワークスペース変数が存在しない場合、個人変数が使用されます:
|
||||
- **手動実行(UI)**:あなたの個人変数
|
||||
- **自動実行(API、ウェブフック、スケジュール、デプロイされたチャット)**:ワークフロー所有者の個人変数
|
||||
|
||||
<Callout type="info">
|
||||
個人変数はテストに最適です。本番環境のワークフローにはワークスペース変数を使用してください。
|
||||
</Callout>
|
||||
|
||||
## セキュリティのベストプラクティス
|
||||
|
||||
### 機密データについて
|
||||
- APIキー、トークン、パスワードはハードコーディングせず、環境変数として保存してください
|
||||
- 複数のチームメンバーが必要とする共有リソースにはワークスペース変数を使用してください
|
||||
- 個人の認証情報は個人変数に保管してください
|
||||
|
||||
### 変数の命名
|
||||
- 説明的な名前を使用する:`DATABASE_URL`ではなく`DB`
|
||||
- チーム全体で一貫した命名規則に従う
|
||||
- 競合を避けるために接頭辞を検討する:`PROD_API_KEY`、`DEV_API_KEY`
|
||||
|
||||
### アクセス制御
|
||||
- ワークスペース環境変数はワークスペースの権限を尊重します
|
||||
- 書き込みアクセス権以上を持つユーザーのみがワークスペース変数を作成/変更できます
|
||||
- 個人変数は常に個々のユーザーにプライベートです
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: 环境变量
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
环境变量为管理工作流中的配置值和密钥(包括 API 密钥和其他敏感数据)提供了一种安全的方式。它们可以在执行期间使用,同时将敏感信息从工作流定义中隔离开来。
|
||||
|
||||
## 变量类型
|
||||
|
||||
Sim 中的环境变量分为两个级别:
|
||||
|
||||
- **个人环境变量**:仅限于您的账户,只有您可以查看和使用
|
||||
- **工作区环境变量**:在整个工作区内共享,所有团队成员都可以使用
|
||||
|
||||
<Callout type="info">
|
||||
当命名冲突时,工作区环境变量优先于个人环境变量。
|
||||
</Callout>
|
||||
|
||||
## 设置环境变量
|
||||
|
||||
前往设置页面配置您的环境变量:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-1.png"
|
||||
alt="用于创建新变量的环境变量弹窗"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
在工作区设置中,您可以创建和管理个人及工作区级别的环境变量。个人变量仅限于您的账户,而工作区变量会与所有团队成员共享。
|
||||
|
||||
### 将变量设为工作区范围
|
||||
|
||||
使用工作区范围切换按钮,使变量对整个团队可用:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-2.png"
|
||||
alt="切换环境变量的工作区范围"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
启用工作区范围后,该变量将对所有工作区成员可用,并可在该工作区内的任何工作流中使用。
|
||||
|
||||
### 工作区变量视图
|
||||
|
||||
一旦您拥有了工作区范围的变量,它们将显示在您的环境变量列表中:
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-3.png"
|
||||
alt="环境变量列表中的工作区范围变量"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## 在工作流中使用变量
|
||||
|
||||
要在工作流中引用环境变量,请使用 `{{}}` 表示法。当您在任何输入字段中键入 `{{` 时,将会出现一个下拉菜单,显示您的个人和工作区级别的环境变量。只需选择您想要使用的变量即可。
|
||||
|
||||
<Image
|
||||
src="/static/environment/environment-4.png"
|
||||
alt="使用双大括号表示法的环境变量"
|
||||
width={500}
|
||||
height={350}
|
||||
/>
|
||||
|
||||
## 变量的解析方式
|
||||
|
||||
**工作区变量始终优先于**个人变量,无论是谁运行工作流。
|
||||
|
||||
当某个键没有工作区变量时,将使用个人变量:
|
||||
- **手动运行(UI)**:使用您的个人变量
|
||||
- **自动运行(API、Webhook、计划任务、已部署的聊天)**:使用工作流所有者的个人变量
|
||||
|
||||
<Callout type="info">
|
||||
个人变量最适合用于测试。生产环境的工作流请使用工作区变量。
|
||||
</Callout>
|
||||
|
||||
## 安全最佳实践
|
||||
|
||||
### 针对敏感数据
|
||||
- 将 API 密钥、令牌和密码存储为环境变量,而不是硬编码它们
|
||||
- 对于多个团队成员需要的共享资源,使用工作区变量
|
||||
- 将个人凭据保存在个人变量中
|
||||
|
||||
### 变量命名
|
||||
- 使用描述性名称:`DATABASE_URL` 而不是 `DB`
|
||||
- 在团队中遵循一致的命名约定
|
||||
- 考虑使用前缀以避免冲突:`PROD_API_KEY`、`DEV_API_KEY`
|
||||
|
||||
### 访问控制
|
||||
- 工作区环境变量遵循工作区权限
|
||||
- 只有具有写入权限或更高权限的用户才能创建/修改工作区变量
|
||||
- 个人变量始终对个人用户私有
|
||||
@@ -21,7 +21,7 @@
|
||||
"fumadocs-mdx": "14.1.0",
|
||||
"fumadocs-ui": "16.2.3",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "16.1.0-canary.21",
|
||||
"next": "16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"postgres": "^3.4.5",
|
||||
"react": "19.2.1",
|
||||
|
||||
BIN
apps/docs/public/static/credentials/create-oauth.png
Normal file
BIN
apps/docs/public/static/credentials/create-oauth.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
BIN
apps/docs/public/static/credentials/create-secret.png
Normal file
BIN
apps/docs/public/static/credentials/create-secret.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
BIN
apps/docs/public/static/credentials/oauth-selector.png
Normal file
BIN
apps/docs/public/static/credentials/oauth-selector.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
apps/docs/public/static/credentials/secret-dropdown.png
Normal file
BIN
apps/docs/public/static/credentials/secret-dropdown.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
apps/docs/public/static/credentials/secret-resolved.png
Normal file
BIN
apps/docs/public/static/credentials/secret-resolved.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
apps/docs/public/static/credentials/settings-secrets.png
Normal file
BIN
apps/docs/public/static/credentials/settings-secrets.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
274
apps/sim/app/(auth)/oauth/consent/page.tsx
Normal file
274
apps/sim/app/(auth)/oauth/consent/page.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ArrowLeftRight } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Button } from '@/components/emcn'
|
||||
import { signOut, useSession } from '@/lib/auth/auth-client'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
|
||||
const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
openid: 'Verify your identity',
|
||||
profile: 'Access your basic profile information',
|
||||
email: 'View your email address',
|
||||
offline_access: 'Maintain access when you are not actively using the app',
|
||||
'mcp:tools': 'Use Sim workflows and tools on your behalf',
|
||||
} as const
|
||||
|
||||
interface ClientInfo {
|
||||
clientId: string
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export default function OAuthConsentPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { data: session } = useSession()
|
||||
const consentCode = searchParams.get('consent_code')
|
||||
const clientId = searchParams.get('client_id')
|
||||
const scope = searchParams.get('scope')
|
||||
|
||||
const [clientInfo, setClientInfo] = useState<ClientInfo | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const scopes = scope?.split(' ').filter(Boolean) ?? []
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) {
|
||||
setLoading(false)
|
||||
setError('The authorization request is missing a required client identifier.')
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`/api/auth/oauth2/client/${encodeURIComponent(clientId)}`, { credentials: 'include' })
|
||||
.then(async (res) => {
|
||||
if (!res.ok) return
|
||||
const data = await res.json()
|
||||
setClientInfo(data)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [clientId])
|
||||
|
||||
const handleConsent = useCallback(
|
||||
async (accept: boolean) => {
|
||||
if (!consentCode) {
|
||||
setError('The authorization request is missing a required consent code.')
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res = await fetch('/api/auth/oauth2/consent', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ accept, consent_code: consentCode }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => null)
|
||||
setError(
|
||||
(body as Record<string, string> | null)?.message ??
|
||||
'The consent request could not be processed. Please try again.'
|
||||
)
|
||||
setSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
const data = (await res.json()) as { redirectURI?: string }
|
||||
if (data.redirectURI) {
|
||||
window.location.href = data.redirectURI
|
||||
} else {
|
||||
setError('The server did not return a redirect. Please try again.')
|
||||
setSubmitting(false)
|
||||
}
|
||||
} catch {
|
||||
setError('Something went wrong. Please try again.')
|
||||
setSubmitting(false)
|
||||
}
|
||||
},
|
||||
[consentCode]
|
||||
)
|
||||
|
||||
const handleSwitchAccount = useCallback(async () => {
|
||||
if (!consentCode) return
|
||||
|
||||
const res = await fetch(`/api/auth/oauth2/authorize-params?consent_code=${consentCode}`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!res.ok) {
|
||||
setError('Unable to switch accounts. Please re-initiate the connection.')
|
||||
return
|
||||
}
|
||||
|
||||
const params = (await res.json()) as Record<string, string | null>
|
||||
const authorizeUrl = new URL('/api/auth/oauth2/authorize', window.location.origin)
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value) authorizeUrl.searchParams.set(key, value)
|
||||
}
|
||||
|
||||
await signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
window.location.href = authorizeUrl.toString()
|
||||
},
|
||||
},
|
||||
})
|
||||
}, [consentCode])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
Authorize Application
|
||||
</h1>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
Loading application details...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
Authorization Error
|
||||
</h1>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
|
||||
<BrandedButton onClick={() => router.push('/')}>Return to Home</BrandedButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const clientName = clientInfo?.name ?? clientId
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='mb-6 flex items-center gap-4'>
|
||||
{clientInfo?.icon ? (
|
||||
<img
|
||||
src={clientInfo.icon}
|
||||
alt={clientName ?? 'Application'}
|
||||
width={48}
|
||||
height={48}
|
||||
className='rounded-[10px]'
|
||||
/>
|
||||
) : (
|
||||
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-muted font-medium text-[18px] text-muted-foreground'>
|
||||
{(clientName ?? '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<ArrowLeftRight className='h-5 w-5 text-muted-foreground' />
|
||||
<Image
|
||||
src='/new/logo/colorized-bg.svg'
|
||||
alt='Sim'
|
||||
width={48}
|
||||
height={48}
|
||||
className='rounded-[10px]'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
Authorize Application
|
||||
</h1>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
<span className='font-medium text-foreground'>{clientName}</span> is requesting access to
|
||||
your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{session?.user && (
|
||||
<div
|
||||
className={`${inter.className} mt-5 flex items-center gap-3 rounded-lg border px-4 py-3`}
|
||||
>
|
||||
{session.user.image ? (
|
||||
<Image
|
||||
src={session.user.image}
|
||||
alt={session.user.name ?? 'User'}
|
||||
width={32}
|
||||
height={32}
|
||||
className='rounded-full'
|
||||
unoptimized
|
||||
/>
|
||||
) : (
|
||||
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-muted font-medium text-[13px] text-muted-foreground'>
|
||||
{(session.user.name ?? session.user.email ?? '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className='min-w-0'>
|
||||
{session.user.name && (
|
||||
<p className='truncate font-medium text-[14px]'>{session.user.name}</p>
|
||||
)}
|
||||
<p className='truncate text-[13px] text-muted-foreground'>{session.user.email}</p>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSwitchAccount}
|
||||
className='ml-auto text-[13px] text-muted-foreground underline-offset-2 transition-colors hover:text-foreground hover:underline'
|
||||
>
|
||||
Switch
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scopes.length > 0 && (
|
||||
<div className={`${inter.className} mt-5 w-full max-w-[410px]`}>
|
||||
<div className='rounded-lg border p-4'>
|
||||
<p className='mb-3 font-medium text-[14px]'>This will allow the application to:</p>
|
||||
<ul className='space-y-2'>
|
||||
{scopes.map((s) => (
|
||||
<li
|
||||
key={s}
|
||||
className='flex items-start gap-2 font-normal text-[13px] text-muted-foreground'
|
||||
>
|
||||
<span className='mt-0.5 text-green-500'>✓</span>
|
||||
<span>{SCOPE_DESCRIPTIONS[s] ?? s}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`${inter.className} mt-6 flex w-full max-w-[410px] gap-3`}>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='md'
|
||||
className='px-6 py-2'
|
||||
disabled={submitting}
|
||||
onClick={() => handleConsent(false)}
|
||||
>
|
||||
Deny
|
||||
</Button>
|
||||
<BrandedButton
|
||||
fullWidth
|
||||
showArrow={false}
|
||||
loading={submitting}
|
||||
loadingText='Authorizing'
|
||||
onClick={() => handleConsent(true)}
|
||||
>
|
||||
Allow
|
||||
</BrandedButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server'
|
||||
import type { NextResponse } from 'next/server'
|
||||
import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
||||
|
||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
||||
return createMcpAuthorizationServerMetadataResponse(request)
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
return createMcpAuthorizationServerMetadataResponse()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server'
|
||||
import type { NextResponse } from 'next/server'
|
||||
import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
||||
|
||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
||||
return createMcpAuthorizationServerMetadataResponse(request)
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
return createMcpAuthorizationServerMetadataResponse()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server'
|
||||
import type { NextResponse } from 'next/server'
|
||||
import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
||||
|
||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
||||
return createMcpAuthorizationServerMetadataResponse(request)
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
return createMcpAuthorizationServerMetadataResponse()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server'
|
||||
import type { NextResponse } from 'next/server'
|
||||
import { createMcpProtectedResourceMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
||||
|
||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
||||
return createMcpProtectedResourceMetadataResponse(request)
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
return createMcpProtectedResourceMetadataResponse()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server'
|
||||
import type { NextResponse } from 'next/server'
|
||||
import { createMcpProtectedResourceMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
||||
|
||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
||||
return createMcpProtectedResourceMetadataResponse(request)
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
return createMcpProtectedResourceMetadataResponse()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createContext, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import posthog from 'posthog-js'
|
||||
import { client } from '@/lib/auth/auth-client'
|
||||
import { extractSessionDataFromAuthClientResult } from '@/lib/auth/session-response'
|
||||
|
||||
export type AppSession = {
|
||||
user: {
|
||||
@@ -45,7 +46,8 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
const res = bypassCache
|
||||
? await client.getSession({ query: { disableCookieCache: true } })
|
||||
: await client.getSession()
|
||||
setData(res?.data ?? null)
|
||||
const session = extractSessionDataFromAuthClientResult(res) as AppSession
|
||||
setData(session)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e : new Error('Failed to fetch session'))
|
||||
} finally {
|
||||
|
||||
@@ -23,7 +23,8 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
pathname.startsWith('/chat') ||
|
||||
pathname.startsWith('/studio') ||
|
||||
pathname.startsWith('/resume') ||
|
||||
pathname.startsWith('/form')
|
||||
pathname.startsWith('/form') ||
|
||||
pathname.startsWith('/oauth')
|
||||
|
||||
return (
|
||||
<NextThemesProvider
|
||||
|
||||
93
apps/sim/app/api/auth/[...all]/route.test.ts
Normal file
93
apps/sim/app/api/auth/[...all]/route.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockRequest, setupCommonApiMocks } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const handlerMocks = vi.hoisted(() => ({
|
||||
betterAuthGET: vi.fn(),
|
||||
betterAuthPOST: vi.fn(),
|
||||
ensureAnonymousUserExists: vi.fn(),
|
||||
createAnonymousGetSessionResponse: vi.fn(() => ({
|
||||
data: {
|
||||
user: { id: 'anon' },
|
||||
session: { id: 'anon-session' },
|
||||
},
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('better-auth/next-js', () => ({
|
||||
toNextJsHandler: () => ({
|
||||
GET: handlerMocks.betterAuthGET,
|
||||
POST: handlerMocks.betterAuthPOST,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { handler: {} },
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/anonymous', () => ({
|
||||
ensureAnonymousUserExists: handlerMocks.ensureAnonymousUserExists,
|
||||
createAnonymousGetSessionResponse: handlerMocks.createAnonymousGetSessionResponse,
|
||||
}))
|
||||
|
||||
describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
setupCommonApiMocks()
|
||||
handlerMocks.betterAuthGET.mockReset()
|
||||
handlerMocks.betterAuthPOST.mockReset()
|
||||
handlerMocks.ensureAnonymousUserExists.mockReset()
|
||||
handlerMocks.createAnonymousGetSessionResponse.mockClear()
|
||||
})
|
||||
|
||||
it('returns anonymous session in better-auth response envelope when auth is disabled', async () => {
|
||||
vi.doMock('@/lib/core/config/feature-flags', () => ({ isAuthDisabled: true }))
|
||||
|
||||
const req = createMockRequest(
|
||||
'GET',
|
||||
undefined,
|
||||
{},
|
||||
'http://localhost:3000/api/auth/get-session'
|
||||
)
|
||||
const { GET } = await import('@/app/api/auth/[...all]/route')
|
||||
|
||||
const res = await GET(req as any)
|
||||
const json = await res.json()
|
||||
|
||||
expect(handlerMocks.ensureAnonymousUserExists).toHaveBeenCalledTimes(1)
|
||||
expect(handlerMocks.betterAuthGET).not.toHaveBeenCalled()
|
||||
expect(json).toEqual({
|
||||
data: {
|
||||
user: { id: 'anon' },
|
||||
session: { id: 'anon-session' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('delegates to better-auth handler when auth is enabled', async () => {
|
||||
vi.doMock('@/lib/core/config/feature-flags', () => ({ isAuthDisabled: false }))
|
||||
|
||||
handlerMocks.betterAuthGET.mockResolvedValueOnce(
|
||||
new (await import('next/server')).NextResponse(JSON.stringify({ data: { ok: true } }), {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}) as any
|
||||
)
|
||||
|
||||
const req = createMockRequest(
|
||||
'GET',
|
||||
undefined,
|
||||
{},
|
||||
'http://localhost:3000/api/auth/get-session'
|
||||
)
|
||||
const { GET } = await import('@/app/api/auth/[...all]/route')
|
||||
|
||||
const res = await GET(req as any)
|
||||
const json = await res.json()
|
||||
|
||||
expect(handlerMocks.ensureAnonymousUserExists).not.toHaveBeenCalled()
|
||||
expect(handlerMocks.betterAuthGET).toHaveBeenCalledTimes(1)
|
||||
expect(json).toEqual({ data: { ok: true } })
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toNextJsHandler } from 'better-auth/next-js'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { createAnonymousSession, ensureAnonymousUserExists } from '@/lib/auth/anonymous'
|
||||
import { createAnonymousGetSessionResponse, ensureAnonymousUserExists } from '@/lib/auth/anonymous'
|
||||
import { isAuthDisabled } from '@/lib/core/config/feature-flags'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -14,7 +14,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
if (path === 'get-session' && isAuthDisabled) {
|
||||
await ensureAnonymousUserExists()
|
||||
return NextResponse.json(createAnonymousSession())
|
||||
return NextResponse.json(createAnonymousGetSessionResponse())
|
||||
}
|
||||
|
||||
return betterAuthGET(request)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
|
||||
@@ -31,15 +31,13 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
.from(account)
|
||||
.where(and(...whereConditions))
|
||||
|
||||
// Use the user's email as the display name (consistent with credential selector)
|
||||
const userEmail = session.user.email
|
||||
.orderBy(desc(account.updatedAt))
|
||||
|
||||
const accountsWithDisplayName = accounts.map((acc) => ({
|
||||
id: acc.id,
|
||||
accountId: acc.accountId,
|
||||
providerId: acc.providerId,
|
||||
displayName: userEmail || acc.providerId,
|
||||
displayName: acc.accountId || acc.providerId,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ accounts: accountsWithDisplayName })
|
||||
|
||||
@@ -57,10 +57,6 @@ describe('OAuth Credentials API Route', () => {
|
||||
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
|
||||
}))
|
||||
|
||||
vi.doMock('jwt-decode', () => ({
|
||||
jwtDecode: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
@@ -84,64 +80,6 @@ describe('OAuth Credentials API Route', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return credentials successfully', async () => {
|
||||
mockGetSession.mockResolvedValueOnce({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
|
||||
mockParseProvider.mockReturnValueOnce({
|
||||
baseProvider: 'google',
|
||||
})
|
||||
|
||||
const mockAccounts = [
|
||||
{
|
||||
id: 'credential-1',
|
||||
userId: 'user-123',
|
||||
providerId: 'google-email',
|
||||
accountId: 'test@example.com',
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
idToken: null,
|
||||
},
|
||||
{
|
||||
id: 'credential-2',
|
||||
userId: 'user-123',
|
||||
providerId: 'google-default',
|
||||
accountId: 'user-id',
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
idToken: null,
|
||||
},
|
||||
]
|
||||
|
||||
mockDb.select.mockReturnValueOnce(mockDb)
|
||||
mockDb.from.mockReturnValueOnce(mockDb)
|
||||
mockDb.where.mockResolvedValueOnce(mockAccounts)
|
||||
|
||||
mockDb.select.mockReturnValueOnce(mockDb)
|
||||
mockDb.from.mockReturnValueOnce(mockDb)
|
||||
mockDb.where.mockReturnValueOnce(mockDb)
|
||||
mockDb.limit.mockResolvedValueOnce([{ email: 'user@example.com' }])
|
||||
|
||||
const req = createMockRequestWithQuery('GET', '?provider=google-email')
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/credentials/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.credentials).toHaveLength(2)
|
||||
expect(data.credentials[0]).toMatchObject({
|
||||
id: 'credential-1',
|
||||
provider: 'google-email',
|
||||
isDefault: false,
|
||||
})
|
||||
expect(data.credentials[1]).toMatchObject({
|
||||
id: 'credential-2',
|
||||
provider: 'google-default',
|
||||
isDefault: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle unauthenticated user', async () => {
|
||||
mockGetSession.mockResolvedValueOnce(null)
|
||||
|
||||
@@ -198,39 +136,12 @@ describe('OAuth Credentials API Route', () => {
|
||||
expect(data.credentials).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should decode ID token for display name', async () => {
|
||||
const { jwtDecode } = await import('jwt-decode')
|
||||
const mockJwtDecode = jwtDecode as any
|
||||
|
||||
it('should return empty credentials when no workspace context', async () => {
|
||||
mockGetSession.mockResolvedValueOnce({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
|
||||
mockParseProvider.mockReturnValueOnce({
|
||||
baseProvider: 'google',
|
||||
})
|
||||
|
||||
const mockAccounts = [
|
||||
{
|
||||
id: 'credential-1',
|
||||
userId: 'user-123',
|
||||
providerId: 'google-default',
|
||||
accountId: 'google-user-id',
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
idToken: 'mock-jwt-token',
|
||||
},
|
||||
]
|
||||
|
||||
mockJwtDecode.mockReturnValueOnce({
|
||||
email: 'decoded@example.com',
|
||||
name: 'Decoded User',
|
||||
})
|
||||
|
||||
mockDb.select.mockReturnValueOnce(mockDb)
|
||||
mockDb.from.mockReturnValueOnce(mockDb)
|
||||
mockDb.where.mockResolvedValueOnce(mockAccounts)
|
||||
|
||||
const req = createMockRequestWithQuery('GET', '?provider=google')
|
||||
const req = createMockRequestWithQuery('GET', '?provider=google-email')
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/credentials/route')
|
||||
|
||||
@@ -238,31 +149,6 @@ describe('OAuth Credentials API Route', () => {
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.credentials[0].name).toBe('decoded@example.com')
|
||||
})
|
||||
|
||||
it('should handle database error', async () => {
|
||||
mockGetSession.mockResolvedValueOnce({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
|
||||
mockParseProvider.mockReturnValueOnce({
|
||||
baseProvider: 'google',
|
||||
})
|
||||
|
||||
mockDb.select.mockReturnValueOnce(mockDb)
|
||||
mockDb.from.mockReturnValueOnce(mockDb)
|
||||
mockDb.where.mockRejectedValueOnce(new Error('Database error'))
|
||||
|
||||
const req = createMockRequestWithQuery('GET', '?provider=google')
|
||||
|
||||
const { GET } = await import('@/app/api/auth/oauth/credentials/route')
|
||||
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.error).toBe('Internal server error')
|
||||
expect(mockLogger.error).toHaveBeenCalled()
|
||||
expect(data.credentials).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { db } from '@sim/db'
|
||||
import { account, user } from '@sim/db/schema'
|
||||
import { account, credential, credentialMember } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { evaluateScopeCoverage, type OAuthProvider, parseProvider } from '@/lib/oauth'
|
||||
import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth'
|
||||
import { evaluateScopeCoverage } from '@/lib/oauth'
|
||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -18,6 +19,7 @@ const credentialsQuerySchema = z
|
||||
.object({
|
||||
provider: z.string().nullish(),
|
||||
workflowId: z.string().uuid('Workflow ID must be a valid UUID').nullish(),
|
||||
workspaceId: z.string().uuid('Workspace ID must be a valid UUID').nullish(),
|
||||
credentialId: z
|
||||
.string()
|
||||
.min(1, 'Credential ID must not be empty')
|
||||
@@ -29,10 +31,30 @@ const credentialsQuerySchema = z
|
||||
path: ['provider'],
|
||||
})
|
||||
|
||||
interface GoogleIdToken {
|
||||
email?: string
|
||||
sub?: string
|
||||
name?: string
|
||||
function toCredentialResponse(
|
||||
id: string,
|
||||
displayName: string,
|
||||
providerId: string,
|
||||
updatedAt: Date,
|
||||
scope: string | null
|
||||
) {
|
||||
const storedScope = scope?.trim()
|
||||
const grantedScopes = storedScope ? storedScope.split(/[\s,]+/).filter(Boolean) : []
|
||||
const scopeEvaluation = evaluateScopeCoverage(providerId, grantedScopes)
|
||||
const [_, featureType = 'default'] = providerId.split('-')
|
||||
|
||||
return {
|
||||
id,
|
||||
name: displayName,
|
||||
provider: providerId,
|
||||
lastUsed: updatedAt.toISOString(),
|
||||
isDefault: featureType === 'default',
|
||||
scopes: scopeEvaluation.grantedScopes,
|
||||
canonicalScopes: scopeEvaluation.canonicalScopes,
|
||||
missingScopes: scopeEvaluation.missingScopes,
|
||||
extraScopes: scopeEvaluation.extraScopes,
|
||||
requiresReauthorization: scopeEvaluation.requiresReauthorization,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,6 +68,7 @@ export async function GET(request: NextRequest) {
|
||||
const rawQuery = {
|
||||
provider: searchParams.get('provider'),
|
||||
workflowId: searchParams.get('workflowId'),
|
||||
workspaceId: searchParams.get('workspaceId'),
|
||||
credentialId: searchParams.get('credentialId'),
|
||||
}
|
||||
|
||||
@@ -78,7 +101,7 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const { provider: providerParam, workflowId, credentialId } = parseResult.data
|
||||
const { provider: providerParam, workflowId, workspaceId, credentialId } = parseResult.data
|
||||
|
||||
// Authenticate requester (supports session and internal JWT)
|
||||
const authResult = await checkSessionOrInternalAuth(request)
|
||||
@@ -88,7 +111,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
const requesterUserId = authResult.userId
|
||||
|
||||
const effectiveUserId = requesterUserId
|
||||
let effectiveWorkspaceId = workspaceId ?? undefined
|
||||
if (workflowId) {
|
||||
const workflowAuthorization = await authorizeWorkflowByWorkspacePermission({
|
||||
workflowId,
|
||||
@@ -106,105 +129,125 @@ export async function GET(request: NextRequest) {
|
||||
{ status: workflowAuthorization.status }
|
||||
)
|
||||
}
|
||||
effectiveWorkspaceId = workflowAuthorization.workflow?.workspaceId || undefined
|
||||
}
|
||||
|
||||
// Parse the provider to get base provider and feature type (if provider is present)
|
||||
const { baseProvider } = parseProvider((providerParam || 'google') as OAuthProvider)
|
||||
|
||||
let accountsData
|
||||
|
||||
if (credentialId && workflowId) {
|
||||
// When both workflowId and credentialId are provided, fetch by ID only.
|
||||
// Workspace authorization above already proves access; the credential
|
||||
// may belong to another workspace member (e.g. for display name resolution).
|
||||
accountsData = await db.select().from(account).where(eq(account.id, credentialId))
|
||||
} else if (credentialId) {
|
||||
accountsData = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(and(eq(account.userId, effectiveUserId), eq(account.id, credentialId)))
|
||||
} else {
|
||||
// Fetch all credentials for provider and effective user
|
||||
accountsData = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(and(eq(account.userId, effectiveUserId), eq(account.providerId, providerParam!)))
|
||||
if (effectiveWorkspaceId) {
|
||||
const workspaceAccess = await checkWorkspaceAccess(effectiveWorkspaceId, requesterUserId)
|
||||
if (!workspaceAccess.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
// Transform accounts into credentials
|
||||
const credentials = await Promise.all(
|
||||
accountsData.map(async (acc) => {
|
||||
// Extract the feature type from providerId (e.g., 'google-default' -> 'default')
|
||||
const [_, featureType = 'default'] = acc.providerId.split('-')
|
||||
if (credentialId) {
|
||||
const [platformCredential] = await db
|
||||
.select({
|
||||
id: credential.id,
|
||||
workspaceId: credential.workspaceId,
|
||||
type: credential.type,
|
||||
displayName: credential.displayName,
|
||||
providerId: credential.providerId,
|
||||
accountId: credential.accountId,
|
||||
accountProviderId: account.providerId,
|
||||
accountScope: account.scope,
|
||||
accountUpdatedAt: account.updatedAt,
|
||||
})
|
||||
.from(credential)
|
||||
.leftJoin(account, eq(credential.accountId, account.id))
|
||||
.where(eq(credential.id, credentialId))
|
||||
.limit(1)
|
||||
|
||||
// Try multiple methods to get a user-friendly display name
|
||||
let displayName = ''
|
||||
if (platformCredential) {
|
||||
if (platformCredential.type !== 'oauth' || !platformCredential.accountId) {
|
||||
return NextResponse.json({ credentials: [] }, { status: 200 })
|
||||
}
|
||||
|
||||
// Method 1: Try to extract email from ID token (works for Google, etc.)
|
||||
if (acc.idToken) {
|
||||
try {
|
||||
const decoded = jwtDecode<GoogleIdToken>(acc.idToken)
|
||||
if (decoded.email) {
|
||||
displayName = decoded.email
|
||||
} else if (decoded.name) {
|
||||
displayName = decoded.name
|
||||
}
|
||||
} catch (_error) {
|
||||
logger.warn(`[${requestId}] Error decoding ID token`, {
|
||||
accountId: acc.id,
|
||||
})
|
||||
if (workflowId) {
|
||||
if (!effectiveWorkspaceId || platformCredential.workspaceId !== effectiveWorkspaceId) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
} else {
|
||||
const [membership] = await db
|
||||
.select({ id: credentialMember.id })
|
||||
.from(credentialMember)
|
||||
.where(
|
||||
and(
|
||||
eq(credentialMember.credentialId, platformCredential.id),
|
||||
eq(credentialMember.userId, requesterUserId),
|
||||
eq(credentialMember.status, 'active')
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!membership) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: For GitHub, the accountId might be the username
|
||||
if (!displayName && baseProvider === 'github') {
|
||||
displayName = `${acc.accountId} (GitHub)`
|
||||
if (!platformCredential.accountProviderId || !platformCredential.accountUpdatedAt) {
|
||||
return NextResponse.json({ credentials: [] }, { status: 200 })
|
||||
}
|
||||
|
||||
// Method 3: Try to get the user's email from our database
|
||||
if (!displayName) {
|
||||
try {
|
||||
const userRecord = await db
|
||||
.select({ email: user.email })
|
||||
.from(user)
|
||||
.where(eq(user.id, acc.userId))
|
||||
.limit(1)
|
||||
return NextResponse.json(
|
||||
{
|
||||
credentials: [
|
||||
toCredentialResponse(
|
||||
platformCredential.id,
|
||||
platformCredential.displayName,
|
||||
platformCredential.accountProviderId,
|
||||
platformCredential.accountUpdatedAt,
|
||||
platformCredential.accountScope
|
||||
),
|
||||
],
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (userRecord.length > 0) {
|
||||
displayName = userRecord[0].email
|
||||
}
|
||||
} catch (_error) {
|
||||
logger.warn(`[${requestId}] Error fetching user email`, {
|
||||
userId: acc.userId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Use accountId with provider type as context
|
||||
if (!displayName) {
|
||||
displayName = `${acc.accountId} (${baseProvider})`
|
||||
}
|
||||
|
||||
const storedScope = acc.scope?.trim()
|
||||
const grantedScopes = storedScope ? storedScope.split(/[\s,]+/).filter(Boolean) : []
|
||||
const scopeEvaluation = evaluateScopeCoverage(acc.providerId, grantedScopes)
|
||||
|
||||
return {
|
||||
id: acc.id,
|
||||
name: displayName,
|
||||
provider: acc.providerId,
|
||||
lastUsed: acc.updatedAt.toISOString(),
|
||||
isDefault: featureType === 'default',
|
||||
scopes: scopeEvaluation.grantedScopes,
|
||||
canonicalScopes: scopeEvaluation.canonicalScopes,
|
||||
missingScopes: scopeEvaluation.missingScopes,
|
||||
extraScopes: scopeEvaluation.extraScopes,
|
||||
requiresReauthorization: scopeEvaluation.requiresReauthorization,
|
||||
}
|
||||
if (effectiveWorkspaceId && providerParam) {
|
||||
await syncWorkspaceOAuthCredentialsForUser({
|
||||
workspaceId: effectiveWorkspaceId,
|
||||
userId: requesterUserId,
|
||||
})
|
||||
)
|
||||
|
||||
return NextResponse.json({ credentials }, { status: 200 })
|
||||
const credentialsData = await db
|
||||
.select({
|
||||
id: credential.id,
|
||||
displayName: credential.displayName,
|
||||
providerId: account.providerId,
|
||||
scope: account.scope,
|
||||
updatedAt: account.updatedAt,
|
||||
})
|
||||
.from(credential)
|
||||
.innerJoin(account, eq(credential.accountId, account.id))
|
||||
.innerJoin(
|
||||
credentialMember,
|
||||
and(
|
||||
eq(credentialMember.credentialId, credential.id),
|
||||
eq(credentialMember.userId, requesterUserId),
|
||||
eq(credentialMember.status, 'active')
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(credential.workspaceId, effectiveWorkspaceId),
|
||||
eq(credential.type, 'oauth'),
|
||||
eq(account.providerId, providerParam)
|
||||
)
|
||||
)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
credentials: credentialsData.map((row) =>
|
||||
toCredentialResponse(row.id, row.displayName, row.providerId, row.updatedAt, row.scope)
|
||||
),
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ credentials: [] }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching OAuth credentials`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
|
||||
@@ -16,6 +16,7 @@ const logger = createLogger('OAuthDisconnectAPI')
|
||||
const disconnectSchema = z.object({
|
||||
provider: z.string({ required_error: 'Provider is required' }).min(1, 'Provider is required'),
|
||||
providerId: z.string().optional(),
|
||||
accountId: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -51,15 +52,20 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const { provider, providerId } = parseResult.data
|
||||
const { provider, providerId, accountId } = parseResult.data
|
||||
|
||||
logger.info(`[${requestId}] Processing OAuth disconnect request`, {
|
||||
provider,
|
||||
hasProviderId: !!providerId,
|
||||
})
|
||||
|
||||
// If a specific providerId is provided, delete only that account
|
||||
if (providerId) {
|
||||
// If a specific account row ID is provided, delete that exact account
|
||||
if (accountId) {
|
||||
await db
|
||||
.delete(account)
|
||||
.where(and(eq(account.userId, session.user.id), eq(account.id, accountId)))
|
||||
} else if (providerId) {
|
||||
// If a specific providerId is provided, delete accounts for that provider ID
|
||||
await db
|
||||
.delete(account)
|
||||
.where(and(eq(account.userId, session.user.id), eq(account.providerId, providerId)))
|
||||
|
||||
@@ -38,13 +38,18 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status })
|
||||
}
|
||||
|
||||
const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId)
|
||||
const resolvedCredentialId = authz.resolvedCredentialId || credentialId
|
||||
const credential = await getCredential(
|
||||
requestId,
|
||||
resolvedCredentialId,
|
||||
authz.credentialOwnerUserId
|
||||
)
|
||||
if (!credential) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credentialId,
|
||||
resolvedCredentialId,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
|
||||
@@ -37,14 +37,19 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status })
|
||||
}
|
||||
|
||||
const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId)
|
||||
const resolvedCredentialId = authz.resolvedCredentialId || credentialId
|
||||
const credential = await getCredential(
|
||||
requestId,
|
||||
resolvedCredentialId,
|
||||
authz.credentialOwnerUserId
|
||||
)
|
||||
if (!credential) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Refresh access token if needed using the utility function
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credentialId,
|
||||
resolvedCredentialId,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockLogger, createMockRequest } from '@sim/testing'
|
||||
import { createMockLogger, createMockRequest, mockHybridAuth } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('OAuth Token API Routes', () => {
|
||||
@@ -12,7 +12,7 @@ describe('OAuth Token API Routes', () => {
|
||||
const mockRefreshTokenIfNeeded = vi.fn()
|
||||
const mockGetOAuthToken = vi.fn()
|
||||
const mockAuthorizeCredentialUse = vi.fn()
|
||||
const mockCheckSessionOrInternalAuth = vi.fn()
|
||||
let mockCheckSessionOrInternalAuth: ReturnType<typeof vi.fn>
|
||||
|
||||
const mockLogger = createMockLogger()
|
||||
|
||||
@@ -41,9 +41,7 @@ describe('OAuth Token API Routes', () => {
|
||||
authorizeCredentialUse: mockAuthorizeCredentialUse,
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
;({ mockCheckSessionOrInternalAuth } = mockHybridAuth())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -73,23 +71,18 @@ describe('OAuth Token API Routes', () => {
|
||||
refreshed: false,
|
||||
})
|
||||
|
||||
// Create mock request
|
||||
const req = createMockRequest('POST', {
|
||||
credentialId: 'credential-id',
|
||||
})
|
||||
|
||||
// Import handler after setting up mocks
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
// Call handler
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
// Verify request was handled correctly
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toHaveProperty('accessToken', 'fresh-token')
|
||||
|
||||
// Verify mocks were called correctly
|
||||
expect(mockAuthorizeCredentialUse).toHaveBeenCalled()
|
||||
expect(mockGetCredential).toHaveBeenCalled()
|
||||
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
|
||||
@@ -351,10 +344,11 @@ describe('OAuth Token API Routes', () => {
|
||||
*/
|
||||
describe('GET handler', () => {
|
||||
it('should return access token successfully', async () => {
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
mockAuthorizeCredentialUse.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
authType: 'session',
|
||||
userId: 'test-user-id',
|
||||
requesterUserId: 'test-user-id',
|
||||
credentialOwnerUserId: 'test-user-id',
|
||||
})
|
||||
mockGetCredential.mockResolvedValueOnce({
|
||||
id: 'credential-id',
|
||||
@@ -380,8 +374,8 @@ describe('OAuth Token API Routes', () => {
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toHaveProperty('accessToken', 'fresh-token')
|
||||
|
||||
expect(mockCheckSessionOrInternalAuth).toHaveBeenCalled()
|
||||
expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id')
|
||||
expect(mockAuthorizeCredentialUse).toHaveBeenCalled()
|
||||
expect(mockGetCredential).toHaveBeenCalled()
|
||||
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -399,8 +393,8 @@ describe('OAuth Token API Routes', () => {
|
||||
})
|
||||
|
||||
it('should handle authentication failure', async () => {
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||
success: false,
|
||||
mockAuthorizeCredentialUse.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
error: 'Authentication required',
|
||||
})
|
||||
|
||||
@@ -413,15 +407,16 @@ describe('OAuth Token API Routes', () => {
|
||||
const response = await GET(req as any)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(response.status).toBe(403)
|
||||
expect(data).toHaveProperty('error')
|
||||
})
|
||||
|
||||
it('should handle credential not found', async () => {
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
mockAuthorizeCredentialUse.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
authType: 'session',
|
||||
userId: 'test-user-id',
|
||||
requesterUserId: 'test-user-id',
|
||||
credentialOwnerUserId: 'test-user-id',
|
||||
})
|
||||
mockGetCredential.mockResolvedValueOnce(undefined)
|
||||
|
||||
@@ -439,10 +434,11 @@ describe('OAuth Token API Routes', () => {
|
||||
})
|
||||
|
||||
it('should handle missing access token', async () => {
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
mockAuthorizeCredentialUse.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
authType: 'session',
|
||||
userId: 'test-user-id',
|
||||
requesterUserId: 'test-user-id',
|
||||
credentialOwnerUserId: 'test-user-id',
|
||||
})
|
||||
mockGetCredential.mockResolvedValueOnce({
|
||||
id: 'credential-id',
|
||||
@@ -465,10 +461,11 @@ describe('OAuth Token API Routes', () => {
|
||||
})
|
||||
|
||||
it('should handle token refresh failure', async () => {
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||
success: true,
|
||||
mockAuthorizeCredentialUse.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
authType: 'session',
|
||||
userId: 'test-user-id',
|
||||
requesterUserId: 'test-user-id',
|
||||
credentialOwnerUserId: 'test-user-id',
|
||||
})
|
||||
mockGetCredential.mockResolvedValueOnce({
|
||||
id: 'credential-id',
|
||||
|
||||
@@ -110,23 +110,35 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const callerUserId = new URL(request.url).searchParams.get('userId') || undefined
|
||||
|
||||
const authz = await authorizeCredentialUse(request, {
|
||||
credentialId,
|
||||
workflowId: workflowId ?? undefined,
|
||||
requireWorkflowIdForInternal: false,
|
||||
callerUserId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId)
|
||||
const resolvedCredentialId = authz.resolvedCredentialId || credentialId
|
||||
const credential = await getCredential(
|
||||
requestId,
|
||||
resolvedCredentialId,
|
||||
authz.credentialOwnerUserId
|
||||
)
|
||||
|
||||
if (!credential) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
|
||||
const { accessToken } = await refreshTokenIfNeeded(
|
||||
requestId,
|
||||
credential,
|
||||
resolvedCredentialId
|
||||
)
|
||||
|
||||
let instanceUrl: string | undefined
|
||||
if (credential.providerId === 'salesforce' && credential.scope) {
|
||||
@@ -186,13 +198,20 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const { credentialId } = parseResult.data
|
||||
|
||||
// For GET requests, we only support session-based authentication
|
||||
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
const authz = await authorizeCredentialUse(request, {
|
||||
credentialId,
|
||||
requireWorkflowIdForInternal: false,
|
||||
})
|
||||
if (!authz.ok || authz.authType !== 'session' || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const credential = await getCredential(requestId, credentialId, auth.userId)
|
||||
const resolvedCredentialId = authz.resolvedCredentialId || credentialId
|
||||
const credential = await getCredential(
|
||||
requestId,
|
||||
resolvedCredentialId,
|
||||
authz.credentialOwnerUserId
|
||||
)
|
||||
|
||||
if (!credential) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
@@ -204,7 +223,11 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
|
||||
const { accessToken } = await refreshTokenIfNeeded(
|
||||
requestId,
|
||||
credential,
|
||||
resolvedCredentialId
|
||||
)
|
||||
|
||||
// For Salesforce, extract instanceUrl from the scope field
|
||||
let instanceUrl: string | undefined
|
||||
|
||||
@@ -62,21 +62,23 @@ describe('OAuth Utils', () => {
|
||||
|
||||
describe('getCredential', () => {
|
||||
it('should return credential when found', async () => {
|
||||
const mockCredential = { id: 'credential-id', userId: 'test-user-id' }
|
||||
const { mockFrom, mockWhere, mockLimit } = mockSelectChain([mockCredential])
|
||||
const mockCredentialRow = { type: 'oauth', accountId: 'resolved-account-id' }
|
||||
const mockAccountRow = { id: 'resolved-account-id', userId: 'test-user-id' }
|
||||
|
||||
mockSelectChain([mockCredentialRow])
|
||||
mockSelectChain([mockAccountRow])
|
||||
|
||||
const credential = await getCredential('request-id', 'credential-id', 'test-user-id')
|
||||
|
||||
expect(mockDb.select).toHaveBeenCalled()
|
||||
expect(mockFrom).toHaveBeenCalled()
|
||||
expect(mockWhere).toHaveBeenCalled()
|
||||
expect(mockLimit).toHaveBeenCalledWith(1)
|
||||
expect(mockDb.select).toHaveBeenCalledTimes(2)
|
||||
|
||||
expect(credential).toEqual(mockCredential)
|
||||
expect(credential).toMatchObject(mockAccountRow)
|
||||
expect(credential).toMatchObject({ resolvedCredentialId: 'resolved-account-id' })
|
||||
})
|
||||
|
||||
it('should return undefined when credential is not found', async () => {
|
||||
mockSelectChain([])
|
||||
mockSelectChain([])
|
||||
|
||||
const credential = await getCredential('request-id', 'nonexistent-id', 'test-user-id')
|
||||
|
||||
@@ -158,15 +160,17 @@ describe('OAuth Utils', () => {
|
||||
|
||||
describe('refreshAccessTokenIfNeeded', () => {
|
||||
it('should return valid access token without refresh if not expired', async () => {
|
||||
const mockCredential = {
|
||||
id: 'credential-id',
|
||||
const mockCredentialRow = { type: 'oauth', accountId: 'account-id' }
|
||||
const mockAccountRow = {
|
||||
id: 'account-id',
|
||||
accessToken: 'valid-token',
|
||||
refreshToken: 'refresh-token',
|
||||
accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
|
||||
providerId: 'google',
|
||||
userId: 'test-user-id',
|
||||
}
|
||||
mockSelectChain([mockCredential])
|
||||
mockSelectChain([mockCredentialRow])
|
||||
mockSelectChain([mockAccountRow])
|
||||
|
||||
const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
|
||||
|
||||
@@ -175,15 +179,17 @@ describe('OAuth Utils', () => {
|
||||
})
|
||||
|
||||
it('should refresh token when expired', async () => {
|
||||
const mockCredential = {
|
||||
id: 'credential-id',
|
||||
const mockCredentialRow = { type: 'oauth', accountId: 'account-id' }
|
||||
const mockAccountRow = {
|
||||
id: 'account-id',
|
||||
accessToken: 'expired-token',
|
||||
refreshToken: 'refresh-token',
|
||||
accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000),
|
||||
providerId: 'google',
|
||||
userId: 'test-user-id',
|
||||
}
|
||||
mockSelectChain([mockCredential])
|
||||
mockSelectChain([mockCredentialRow])
|
||||
mockSelectChain([mockAccountRow])
|
||||
mockUpdateChain()
|
||||
|
||||
mockRefreshOAuthToken.mockResolvedValueOnce({
|
||||
@@ -201,6 +207,7 @@ describe('OAuth Utils', () => {
|
||||
|
||||
it('should return null if credential not found', async () => {
|
||||
mockSelectChain([])
|
||||
mockSelectChain([])
|
||||
|
||||
const token = await refreshAccessTokenIfNeeded('nonexistent-id', 'test-user-id', 'request-id')
|
||||
|
||||
@@ -208,15 +215,17 @@ describe('OAuth Utils', () => {
|
||||
})
|
||||
|
||||
it('should return null if refresh fails', async () => {
|
||||
const mockCredential = {
|
||||
id: 'credential-id',
|
||||
const mockCredentialRow = { type: 'oauth', accountId: 'account-id' }
|
||||
const mockAccountRow = {
|
||||
id: 'account-id',
|
||||
accessToken: 'expired-token',
|
||||
refreshToken: 'refresh-token',
|
||||
accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000),
|
||||
providerId: 'google',
|
||||
userId: 'test-user-id',
|
||||
}
|
||||
mockSelectChain([mockCredential])
|
||||
mockSelectChain([mockCredentialRow])
|
||||
mockSelectChain([mockAccountRow])
|
||||
|
||||
mockRefreshOAuthToken.mockResolvedValueOnce(null)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '@sim/db'
|
||||
import { account, credentialSetMember } from '@sim/db/schema'
|
||||
import { account, credential, credentialSetMember } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq, inArray } from 'drizzle-orm'
|
||||
import { refreshOAuthToken } from '@/lib/oauth'
|
||||
@@ -25,6 +25,38 @@ interface AccountInsertData {
|
||||
accessTokenExpiresAt?: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a credential ID to its underlying account ID.
|
||||
* If `credentialId` matches a `credential` row, returns its `accountId` and `workspaceId`.
|
||||
* Otherwise assumes `credentialId` is already a raw `account.id` (legacy).
|
||||
*/
|
||||
export async function resolveOAuthAccountId(
|
||||
credentialId: string
|
||||
): Promise<{ accountId: string; workspaceId?: string; usedCredentialTable: boolean } | null> {
|
||||
const [credentialRow] = await db
|
||||
.select({
|
||||
type: credential.type,
|
||||
accountId: credential.accountId,
|
||||
workspaceId: credential.workspaceId,
|
||||
})
|
||||
.from(credential)
|
||||
.where(eq(credential.id, credentialId))
|
||||
.limit(1)
|
||||
|
||||
if (credentialRow) {
|
||||
if (credentialRow.type !== 'oauth' || !credentialRow.accountId) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
accountId: credentialRow.accountId,
|
||||
workspaceId: credentialRow.workspaceId,
|
||||
usedCredentialTable: true,
|
||||
}
|
||||
}
|
||||
|
||||
return { accountId: credentialId, usedCredentialTable: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely inserts an account record, handling duplicate constraint violations gracefully.
|
||||
* If a duplicate is detected (unique constraint violation), logs a warning and returns success.
|
||||
@@ -52,10 +84,16 @@ export async function safeAccountInsert(
|
||||
* Get a credential by ID and verify it belongs to the user
|
||||
*/
|
||||
export async function getCredential(requestId: string, credentialId: string, userId: string) {
|
||||
const resolved = await resolveOAuthAccountId(credentialId)
|
||||
if (!resolved) {
|
||||
logger.warn(`[${requestId}] Credential is not an OAuth credential`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const credentials = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(and(eq(account.id, credentialId), eq(account.userId, userId)))
|
||||
.where(and(eq(account.id, resolved.accountId), eq(account.userId, userId)))
|
||||
.limit(1)
|
||||
|
||||
if (!credentials.length) {
|
||||
@@ -63,7 +101,10 @@ export async function getCredential(requestId: string, credentialId: string, use
|
||||
return undefined
|
||||
}
|
||||
|
||||
return credentials[0]
|
||||
return {
|
||||
...credentials[0],
|
||||
resolvedCredentialId: resolved.accountId,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOAuthToken(userId: string, providerId: string): Promise<string | null> {
|
||||
@@ -238,7 +279,9 @@ export async function refreshAccessTokenIfNeeded(
|
||||
}
|
||||
|
||||
// Update the token in the database
|
||||
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
||||
const resolvedCredentialId =
|
||||
(credential as { resolvedCredentialId?: string }).resolvedCredentialId ?? credentialId
|
||||
await db.update(account).set(updateData).where(eq(account.id, resolvedCredentialId))
|
||||
|
||||
logger.info(`[${requestId}] Successfully refreshed access token for credential`)
|
||||
return refreshedToken.accessToken
|
||||
@@ -274,6 +317,8 @@ export async function refreshTokenIfNeeded(
|
||||
credential: any,
|
||||
credentialId: string
|
||||
): Promise<{ accessToken: string; refreshed: boolean }> {
|
||||
const resolvedCredentialId = credential.resolvedCredentialId ?? credentialId
|
||||
|
||||
// Decide if we should refresh: token missing OR expired
|
||||
const accessTokenExpiresAt = credential.accessTokenExpiresAt
|
||||
const refreshTokenExpiresAt = credential.refreshTokenExpiresAt
|
||||
@@ -334,7 +379,7 @@ export async function refreshTokenIfNeeded(
|
||||
updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
|
||||
}
|
||||
|
||||
await db.update(account).set(updateData).where(eq(account.id, credentialId))
|
||||
await db.update(account).set(updateData).where(eq(account.id, resolvedCredentialId))
|
||||
|
||||
logger.info(`[${requestId}] Successfully refreshed access token`)
|
||||
return { accessToken: refreshedToken, refreshed: true }
|
||||
@@ -343,7 +388,7 @@ export async function refreshTokenIfNeeded(
|
||||
`[${requestId}] Refresh attempt failed, checking if another concurrent request succeeded`
|
||||
)
|
||||
|
||||
const freshCredential = await getCredential(requestId, credentialId, credential.userId)
|
||||
const freshCredential = await getCredential(requestId, resolvedCredentialId, credential.userId)
|
||||
if (freshCredential?.accessToken) {
|
||||
const freshExpiresAt = freshCredential.accessTokenExpiresAt
|
||||
const stillValid = !freshExpiresAt || freshExpiresAt > new Date()
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -57,24 +57,41 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: itemIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
const resolved = await resolveOAuthAccountId(credentialId)
|
||||
if (!resolved) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (resolved.workspaceId) {
|
||||
const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils')
|
||||
const perm = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
resolved.workspaceId
|
||||
)
|
||||
if (perm === null) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(eq(account.id, resolved.accountId))
|
||||
.limit(1)
|
||||
|
||||
if (!credentials.length) {
|
||||
logger.warn(`[${requestId}] Credential not found`, { credentialId })
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const credential = credentials[0]
|
||||
const accountRow = credentials[0]
|
||||
|
||||
if (credential.userId !== session.user.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
|
||||
credentialUserId: credential.userId,
|
||||
requestUserId: session.user.id,
|
||||
})
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
resolved.accountId,
|
||||
accountRow.userId,
|
||||
requestId
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error(`[${requestId}] Failed to obtain valid access token`)
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -47,27 +47,41 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Get the credential from the database
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
const resolved = await resolveOAuthAccountId(credentialId)
|
||||
if (!resolved) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (resolved.workspaceId) {
|
||||
const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils')
|
||||
const perm = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
resolved.workspaceId
|
||||
)
|
||||
if (perm === null) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(eq(account.id, resolved.accountId))
|
||||
.limit(1)
|
||||
|
||||
if (!credentials.length) {
|
||||
logger.warn(`[${requestId}] Credential not found`, { credentialId })
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const credential = credentials[0]
|
||||
const accountRow = credentials[0]
|
||||
|
||||
// Check if the credential belongs to the user
|
||||
if (credential.userId !== session.user.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
|
||||
credentialUserId: credential.userId,
|
||||
requestUserId: session.user.id,
|
||||
})
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Refresh access token if needed
|
||||
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
resolved.accountId,
|
||||
accountRow.userId,
|
||||
requestId
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error(`[${requestId}] Failed to obtain valid access token`)
|
||||
|
||||
59
apps/sim/app/api/auth/oauth2/authorize-params/route.ts
Normal file
59
apps/sim/app/api/auth/oauth2/authorize-params/route.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { db } from '@sim/db'
|
||||
import { verification } from '@sim/db/schema'
|
||||
import { and, eq, gt } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
|
||||
/**
|
||||
* Returns the original OAuth authorize parameters stored in the verification record
|
||||
* for a given consent code. Used by the consent page to reconstruct the authorize URL
|
||||
* when switching accounts.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await getSession()
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const consentCode = request.nextUrl.searchParams.get('consent_code')
|
||||
if (!consentCode) {
|
||||
return NextResponse.json({ error: 'consent_code is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const [record] = await db
|
||||
.select({ value: verification.value })
|
||||
.from(verification)
|
||||
.where(and(eq(verification.identifier, consentCode), gt(verification.expiresAt, new Date())))
|
||||
.limit(1)
|
||||
|
||||
if (!record) {
|
||||
return NextResponse.json({ error: 'Invalid or expired consent code' }, { status: 404 })
|
||||
}
|
||||
|
||||
const data = JSON.parse(record.value) as {
|
||||
clientId: string
|
||||
redirectURI: string
|
||||
scope: string[]
|
||||
userId: string
|
||||
codeChallenge: string
|
||||
codeChallengeMethod: string
|
||||
state: string | null
|
||||
nonce: string | null
|
||||
}
|
||||
|
||||
if (data.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
client_id: data.clientId,
|
||||
redirect_uri: data.redirectURI,
|
||||
scope: data.scope.join(' '),
|
||||
code_challenge: data.codeChallenge,
|
||||
code_challenge_method: data.codeChallengeMethod,
|
||||
state: data.state,
|
||||
nonce: data.nonce,
|
||||
response_type: 'code',
|
||||
})
|
||||
}
|
||||
@@ -48,16 +48,21 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const shopData = await shopResponse.json()
|
||||
const shopInfo = shopData.shop
|
||||
const stableAccountId = shopInfo.id?.toString() || shopDomain
|
||||
|
||||
const existing = await db.query.account.findFirst({
|
||||
where: and(eq(account.userId, session.user.id), eq(account.providerId, 'shopify')),
|
||||
where: and(
|
||||
eq(account.userId, session.user.id),
|
||||
eq(account.providerId, 'shopify'),
|
||||
eq(account.accountId, stableAccountId)
|
||||
),
|
||||
})
|
||||
|
||||
const now = new Date()
|
||||
|
||||
const accountData = {
|
||||
accessToken: accessToken,
|
||||
accountId: shopInfo.id?.toString() || shopDomain,
|
||||
accountId: stableAccountId,
|
||||
scope: scope || '',
|
||||
updatedAt: now,
|
||||
idToken: shopDomain,
|
||||
|
||||
@@ -52,7 +52,11 @@ export async function POST(request: NextRequest) {
|
||||
const trelloUser = await userResponse.json()
|
||||
|
||||
const existing = await db.query.account.findFirst({
|
||||
where: and(eq(account.userId, session.user.id), eq(account.providerId, 'trello')),
|
||||
where: and(
|
||||
eq(account.userId, session.user.id),
|
||||
eq(account.providerId, 'trello'),
|
||||
eq(account.accountId, trelloUser.id)
|
||||
),
|
||||
})
|
||||
|
||||
const now = new Date()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getCreditBalance } from '@/lib/billing/credits/balance'
|
||||
import { purchaseCredits } from '@/lib/billing/credits/purchase'
|
||||
@@ -57,6 +58,17 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
}
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDIT_PURCHASED,
|
||||
resourceType: AuditResourceType.BILLING,
|
||||
description: `Purchased $${validation.data.amount} in credits`,
|
||||
metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Failed to purchase credits', { error, userId: session.user.id })
|
||||
|
||||
@@ -33,7 +33,6 @@ export async function POST(req: NextRequest) {
|
||||
logger.info(`[${requestId}] Update cost request started`)
|
||||
|
||||
if (!isBillingEnabled) {
|
||||
logger.debug(`[${requestId}] Billing is disabled, skipping cost update`)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Billing disabled, cost update skipped',
|
||||
|
||||
@@ -117,8 +117,6 @@ export async function POST(
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
logger.debug(`[${requestId}] Processing OTP request for identifier: ${identifier}`)
|
||||
|
||||
const body = await request.json()
|
||||
const { email } = otpRequestSchema.parse(body)
|
||||
|
||||
@@ -211,8 +209,6 @@ export async function PUT(
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
logger.debug(`[${requestId}] Verifying OTP for identifier: ${identifier}`)
|
||||
|
||||
const body = await request.json()
|
||||
const { email, otp } = otpVerifySchema.parse(body)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { loggerMock } from '@sim/testing'
|
||||
import { loggerMock, requestUtilsMock } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
/**
|
||||
@@ -94,9 +94,7 @@ vi.mock('@/lib/core/utils/sse', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn().mockReturnValue('test-request-id'),
|
||||
}))
|
||||
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
vi.mock('@/lib/core/security/encryption', () => ({
|
||||
decryptSecret: vi.fn().mockResolvedValue({ decrypted: 'test-password' }),
|
||||
|
||||
@@ -42,8 +42,6 @@ export async function POST(
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
logger.debug(`[${requestId}] Processing chat request for identifier: ${identifier}`)
|
||||
|
||||
let parsedBody
|
||||
try {
|
||||
const rawBody = await request.json()
|
||||
@@ -294,8 +292,6 @@ export async function GET(
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
logger.debug(`[${requestId}] Fetching chat info for identifier: ${identifier}`)
|
||||
|
||||
const deploymentResult = await db
|
||||
.select({
|
||||
id: chat.id,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { databaseMock, loggerMock } from '@sim/testing'
|
||||
import { databaseMock, loggerMock, requestUtilsMock } from '@sim/testing'
|
||||
import type { NextResponse } from 'next/server'
|
||||
/**
|
||||
* Tests for chat API utils
|
||||
@@ -37,9 +37,7 @@ vi.mock('@/lib/core/security/encryption', () => ({
|
||||
decryptSecret: mockDecryptSecret,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
vi.mock('@/lib/core/config/feature-flags', () => ({
|
||||
isDev: true,
|
||||
|
||||
@@ -95,11 +95,6 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const data = CreateCreatorProfileSchema.parse(body)
|
||||
|
||||
logger.debug(`[${requestId}] Creating creator profile:`, {
|
||||
referenceType: data.referenceType,
|
||||
referenceId: data.referenceId,
|
||||
})
|
||||
|
||||
// Validate permissions
|
||||
if (data.referenceType === 'user') {
|
||||
if (data.referenceId !== session.user.id) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { hasCredentialSetsAccess } from '@/lib/billing'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
@@ -148,6 +149,20 @@ export async function POST(
|
||||
userId: session.user.id,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
workspaceId: null,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_SET_INVITATION_RESENT,
|
||||
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||
resourceId: id,
|
||||
resourceName: result.set.name,
|
||||
description: `Resent credential set invitation to ${invitation.email}`,
|
||||
metadata: { invitationId, targetEmail: invitation.email },
|
||||
request: req,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Error resending invitation', error)
|
||||
|
||||
@@ -186,6 +186,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: result.set.name,
|
||||
description: `Created invitation for credential set "${result.set.name}"${email ? ` to ${email}` : ''}`,
|
||||
metadata: { targetEmail: email || undefined },
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -239,7 +240,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 })
|
||||
}
|
||||
|
||||
await db
|
||||
const [revokedInvitation] = await db
|
||||
.update(credentialSetInvitation)
|
||||
.set({ status: 'cancelled' })
|
||||
.where(
|
||||
@@ -248,6 +249,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
eq(credentialSetInvitation.credentialSetId, id)
|
||||
)
|
||||
)
|
||||
.returning({ email: credentialSetInvitation.email })
|
||||
|
||||
recordAudit({
|
||||
workspaceId: null,
|
||||
@@ -259,6 +261,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: result.set.name,
|
||||
description: `Revoked invitation "${invitationId}" for credential set "${result.set.name}"`,
|
||||
metadata: { targetEmail: revokedInvitation?.email ?? undefined },
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -151,8 +151,15 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
}
|
||||
|
||||
const [memberToRemove] = await db
|
||||
.select()
|
||||
.select({
|
||||
id: credentialSetMember.id,
|
||||
credentialSetId: credentialSetMember.credentialSetId,
|
||||
userId: credentialSetMember.userId,
|
||||
status: credentialSetMember.status,
|
||||
email: user.email,
|
||||
})
|
||||
.from(credentialSetMember)
|
||||
.innerJoin(user, eq(credentialSetMember.userId, user.id))
|
||||
.where(and(eq(credentialSetMember.id, memberId), eq(credentialSetMember.credentialSetId, id)))
|
||||
.limit(1)
|
||||
|
||||
@@ -189,6 +196,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: result.set.name,
|
||||
description: `Removed member from credential set "${result.set.name}"`,
|
||||
metadata: { targetEmail: memberToRemove.email ?? undefined },
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||
|
||||
@@ -78,6 +79,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
status: credentialSetInvitation.status,
|
||||
expiresAt: credentialSetInvitation.expiresAt,
|
||||
invitedBy: credentialSetInvitation.invitedBy,
|
||||
credentialSetName: credentialSet.name,
|
||||
providerId: credentialSet.providerId,
|
||||
})
|
||||
.from(credentialSetInvitation)
|
||||
@@ -125,7 +127,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
const now = new Date()
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
// Use transaction to ensure membership + invitation update + webhook sync are atomic
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(credentialSetMember).values({
|
||||
id: crypto.randomUUID(),
|
||||
@@ -147,8 +148,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
})
|
||||
.where(eq(credentialSetInvitation.id, invitation.id))
|
||||
|
||||
// Clean up all other pending invitations for the same credential set and email
|
||||
// This prevents duplicate invites from showing up after accepting one
|
||||
if (invitation.email) {
|
||||
await tx
|
||||
.update(credentialSetInvitation)
|
||||
@@ -166,7 +165,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
)
|
||||
}
|
||||
|
||||
// Sync webhooks within the transaction
|
||||
const syncResult = await syncAllWebhooksForCredentialSet(
|
||||
invitation.credentialSetId,
|
||||
requestId,
|
||||
@@ -184,6 +182,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
userId: session.user.id,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_SET_INVITATION_ACCEPTED,
|
||||
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||
resourceId: invitation.credentialSetId,
|
||||
resourceName: invitation.credentialSetName,
|
||||
description: `Accepted credential set invitation`,
|
||||
metadata: { invitationId: invitation.id },
|
||||
request: req,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
credentialSetId: invitation.credentialSetId,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { credentialSet, credentialSetMember, organization } from '@sim/db/schema
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||
|
||||
@@ -106,6 +107,17 @@ export async function DELETE(req: NextRequest) {
|
||||
userId: session.user.id,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_SET_MEMBER_LEFT,
|
||||
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||
resourceId: credentialSetId,
|
||||
description: `Left credential set`,
|
||||
request: req,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to leave credential set'
|
||||
|
||||
226
apps/sim/app/api/credentials/[id]/members/route.ts
Normal file
226
apps/sim/app/api/credentials/[id]/members/route.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { db } from '@sim/db'
|
||||
import { credential, credentialMember, user } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('CredentialMembersAPI')
|
||||
|
||||
interface RouteContext {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
async function requireWorkspaceAdminMembership(credentialId: string, userId: string) {
|
||||
const [cred] = await db
|
||||
.select({ id: credential.id, workspaceId: credential.workspaceId })
|
||||
.from(credential)
|
||||
.where(eq(credential.id, credentialId))
|
||||
.limit(1)
|
||||
|
||||
if (!cred) return null
|
||||
|
||||
const perm = await getUserEntityPermissions(userId, 'workspace', cred.workspaceId)
|
||||
if (perm === null) return null
|
||||
|
||||
const [membership] = await db
|
||||
.select({ role: credentialMember.role, status: credentialMember.status })
|
||||
.from(credentialMember)
|
||||
.where(
|
||||
and(eq(credentialMember.credentialId, credentialId), eq(credentialMember.userId, userId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!membership || membership.status !== 'active' || membership.role !== 'admin') {
|
||||
return null
|
||||
}
|
||||
return membership
|
||||
}
|
||||
|
||||
export async function GET(_request: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: credentialId } = await context.params
|
||||
|
||||
const [cred] = await db
|
||||
.select({ id: credential.id, workspaceId: credential.workspaceId })
|
||||
.from(credential)
|
||||
.where(eq(credential.id, credentialId))
|
||||
.limit(1)
|
||||
|
||||
if (!cred) {
|
||||
return NextResponse.json({ members: [] }, { status: 200 })
|
||||
}
|
||||
|
||||
const callerPerm = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
cred.workspaceId
|
||||
)
|
||||
if (callerPerm === null) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const members = await db
|
||||
.select({
|
||||
id: credentialMember.id,
|
||||
userId: credentialMember.userId,
|
||||
role: credentialMember.role,
|
||||
status: credentialMember.status,
|
||||
joinedAt: credentialMember.joinedAt,
|
||||
userName: user.name,
|
||||
userEmail: user.email,
|
||||
})
|
||||
.from(credentialMember)
|
||||
.innerJoin(user, eq(credentialMember.userId, user.id))
|
||||
.where(eq(credentialMember.credentialId, credentialId))
|
||||
|
||||
return NextResponse.json({ members })
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch credential members', { error })
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
const addMemberSchema = z.object({
|
||||
userId: z.string().min(1),
|
||||
role: z.enum(['admin', 'member']).default('member'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: credentialId } = await context.params
|
||||
|
||||
const admin = await requireWorkspaceAdminMembership(credentialId, session.user.id)
|
||||
if (!admin) {
|
||||
return NextResponse.json({ error: 'Admin access required' }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const parsed = addMemberSchema.safeParse(body)
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { userId, role } = parsed.data
|
||||
const now = new Date()
|
||||
|
||||
const [existing] = await db
|
||||
.select({ id: credentialMember.id, status: credentialMember.status })
|
||||
.from(credentialMember)
|
||||
.where(
|
||||
and(eq(credentialMember.credentialId, credentialId), eq(credentialMember.userId, userId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (existing) {
|
||||
await db
|
||||
.update(credentialMember)
|
||||
.set({ role, status: 'active', updatedAt: now })
|
||||
.where(eq(credentialMember.id, existing.id))
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
|
||||
await db.insert(credentialMember).values({
|
||||
id: crypto.randomUUID(),
|
||||
credentialId,
|
||||
userId,
|
||||
role,
|
||||
status: 'active',
|
||||
joinedAt: now,
|
||||
invitedBy: session.user.id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 201 })
|
||||
} catch (error) {
|
||||
logger.error('Failed to add credential member', { error })
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest, context: RouteContext) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: credentialId } = await context.params
|
||||
const targetUserId = new URL(request.url).searchParams.get('userId')
|
||||
if (!targetUserId) {
|
||||
return NextResponse.json({ error: 'userId query parameter required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const admin = await requireWorkspaceAdminMembership(credentialId, session.user.id)
|
||||
if (!admin) {
|
||||
return NextResponse.json({ error: 'Admin access required' }, { status: 403 })
|
||||
}
|
||||
|
||||
const [target] = await db
|
||||
.select({
|
||||
id: credentialMember.id,
|
||||
role: credentialMember.role,
|
||||
})
|
||||
.from(credentialMember)
|
||||
.where(
|
||||
and(
|
||||
eq(credentialMember.credentialId, credentialId),
|
||||
eq(credentialMember.userId, targetUserId),
|
||||
eq(credentialMember.status, 'active')
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!target) {
|
||||
return NextResponse.json({ error: 'Member not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const revoked = await db.transaction(async (tx) => {
|
||||
if (target.role === 'admin') {
|
||||
const activeAdmins = await tx
|
||||
.select({ id: credentialMember.id })
|
||||
.from(credentialMember)
|
||||
.where(
|
||||
and(
|
||||
eq(credentialMember.credentialId, credentialId),
|
||||
eq(credentialMember.role, 'admin'),
|
||||
eq(credentialMember.status, 'active')
|
||||
)
|
||||
)
|
||||
|
||||
if (activeAdmins.length <= 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(credentialMember)
|
||||
.set({ status: 'revoked', updatedAt: new Date() })
|
||||
.where(eq(credentialMember.id, target.id))
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (!revoked) {
|
||||
return NextResponse.json({ error: 'Cannot remove the last admin' }, { status: 400 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Failed to remove credential member', { error })
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
251
apps/sim/app/api/credentials/[id]/route.ts
Normal file
251
apps/sim/app/api/credentials/[id]/route.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { db } from '@sim/db'
|
||||
import { credential, credentialMember, environment, workspaceEnvironment } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getCredentialActorContext } from '@/lib/credentials/access'
|
||||
import {
|
||||
syncPersonalEnvCredentialsForUser,
|
||||
syncWorkspaceEnvCredentials,
|
||||
} from '@/lib/credentials/environment'
|
||||
|
||||
const logger = createLogger('CredentialByIdAPI')
|
||||
|
||||
const updateCredentialSchema = z
|
||||
.object({
|
||||
displayName: z.string().trim().min(1).max(255).optional(),
|
||||
description: z.string().trim().max(500).nullish(),
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => data.displayName !== undefined || data.description !== undefined, {
|
||||
message: 'At least one field must be provided',
|
||||
path: ['displayName'],
|
||||
})
|
||||
|
||||
async function getCredentialResponse(credentialId: string, userId: string) {
|
||||
const [row] = await db
|
||||
.select({
|
||||
id: credential.id,
|
||||
workspaceId: credential.workspaceId,
|
||||
type: credential.type,
|
||||
displayName: credential.displayName,
|
||||
description: credential.description,
|
||||
providerId: credential.providerId,
|
||||
accountId: credential.accountId,
|
||||
envKey: credential.envKey,
|
||||
envOwnerUserId: credential.envOwnerUserId,
|
||||
createdBy: credential.createdBy,
|
||||
createdAt: credential.createdAt,
|
||||
updatedAt: credential.updatedAt,
|
||||
role: credentialMember.role,
|
||||
status: credentialMember.status,
|
||||
})
|
||||
.from(credential)
|
||||
.innerJoin(
|
||||
credentialMember,
|
||||
and(eq(credentialMember.credentialId, credential.id), eq(credentialMember.userId, userId))
|
||||
)
|
||||
.where(eq(credential.id, credentialId))
|
||||
.limit(1)
|
||||
|
||||
return row ?? null
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const access = await getCredentialActorContext(id, session.user.id)
|
||||
if (!access.credential) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
if (!access.hasWorkspaceAccess || !access.member) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const row = await getCredentialResponse(id, session.user.id)
|
||||
return NextResponse.json({ credential: row }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch credential', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const parseResult = updateCredentialSchema.safeParse(await request.json())
|
||||
if (!parseResult.success) {
|
||||
return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const access = await getCredentialActorContext(id, session.user.id)
|
||||
if (!access.credential) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
if (!access.hasWorkspaceAccess || !access.isAdmin) {
|
||||
return NextResponse.json({ error: 'Credential admin permission required' }, { status: 403 })
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = {}
|
||||
|
||||
if (parseResult.data.description !== undefined) {
|
||||
updates.description = parseResult.data.description ?? null
|
||||
}
|
||||
|
||||
if (parseResult.data.displayName !== undefined && access.credential.type === 'oauth') {
|
||||
updates.displayName = parseResult.data.displayName
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
if (access.credential.type === 'oauth') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'No updatable fields provided.',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'Environment credentials cannot be updated via this endpoint. Use the environment value editor in credentials settings.',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
updates.updatedAt = new Date()
|
||||
await db.update(credential).set(updates).where(eq(credential.id, id))
|
||||
|
||||
const row = await getCredentialResponse(id, session.user.id)
|
||||
return NextResponse.json({ credential: row }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error('Failed to update credential', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const access = await getCredentialActorContext(id, session.user.id)
|
||||
if (!access.credential) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
if (!access.hasWorkspaceAccess || !access.isAdmin) {
|
||||
return NextResponse.json({ error: 'Credential admin permission required' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (access.credential.type === 'env_personal' && access.credential.envKey) {
|
||||
const ownerUserId = access.credential.envOwnerUserId
|
||||
if (!ownerUserId) {
|
||||
return NextResponse.json({ error: 'Invalid personal secret owner' }, { status: 400 })
|
||||
}
|
||||
|
||||
const [personalRow] = await db
|
||||
.select({ variables: environment.variables })
|
||||
.from(environment)
|
||||
.where(eq(environment.userId, ownerUserId))
|
||||
.limit(1)
|
||||
|
||||
const current = ((personalRow?.variables as Record<string, string> | null) ?? {}) as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
if (access.credential.envKey in current) {
|
||||
delete current[access.credential.envKey]
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(environment)
|
||||
.values({
|
||||
id: ownerUserId,
|
||||
userId: ownerUserId,
|
||||
variables: current,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [environment.userId],
|
||||
set: { variables: current, updatedAt: new Date() },
|
||||
})
|
||||
|
||||
await syncPersonalEnvCredentialsForUser({
|
||||
userId: ownerUserId,
|
||||
envKeys: Object.keys(current),
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
}
|
||||
|
||||
if (access.credential.type === 'env_workspace' && access.credential.envKey) {
|
||||
const [workspaceRow] = await db
|
||||
.select({
|
||||
id: workspaceEnvironment.id,
|
||||
createdAt: workspaceEnvironment.createdAt,
|
||||
variables: workspaceEnvironment.variables,
|
||||
})
|
||||
.from(workspaceEnvironment)
|
||||
.where(eq(workspaceEnvironment.workspaceId, access.credential.workspaceId))
|
||||
.limit(1)
|
||||
|
||||
const current = ((workspaceRow?.variables as Record<string, string> | null) ?? {}) as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
if (access.credential.envKey in current) {
|
||||
delete current[access.credential.envKey]
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(workspaceEnvironment)
|
||||
.values({
|
||||
id: workspaceRow?.id || crypto.randomUUID(),
|
||||
workspaceId: access.credential.workspaceId,
|
||||
variables: current,
|
||||
createdAt: workspaceRow?.createdAt || new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [workspaceEnvironment.workspaceId],
|
||||
set: { variables: current, updatedAt: new Date() },
|
||||
})
|
||||
|
||||
await syncWorkspaceEnvCredentials({
|
||||
workspaceId: access.credential.workspaceId,
|
||||
envKeys: Object.keys(current),
|
||||
actingUserId: session.user.id,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
}
|
||||
|
||||
await db.delete(credential).where(eq(credential.id, id))
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete credential', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
116
apps/sim/app/api/credentials/draft/route.ts
Normal file
116
apps/sim/app/api/credentials/draft/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { db } from '@sim/db'
|
||||
import { credential, credentialMember, pendingCredentialDraft } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, lt } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('CredentialDraftAPI')
|
||||
|
||||
const DRAFT_TTL_MS = 15 * 60 * 1000
|
||||
|
||||
const createDraftSchema = z.object({
|
||||
workspaceId: z.string().min(1),
|
||||
providerId: z.string().min(1),
|
||||
displayName: z.string().min(1),
|
||||
description: z.string().trim().max(500).optional(),
|
||||
credentialId: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const parsed = createDraftSchema.safeParse(body)
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { workspaceId, providerId, displayName, description, credentialId } = parsed.data
|
||||
const userId = session.user.id
|
||||
|
||||
const workspaceAccess = await checkWorkspaceAccess(workspaceId, userId)
|
||||
if (!workspaceAccess.canWrite) {
|
||||
return NextResponse.json({ error: 'Write permission required' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (credentialId) {
|
||||
const [membership] = await db
|
||||
.select({ role: credentialMember.role, status: credentialMember.status })
|
||||
.from(credentialMember)
|
||||
.innerJoin(credential, eq(credential.id, credentialMember.credentialId))
|
||||
.where(
|
||||
and(
|
||||
eq(credentialMember.credentialId, credentialId),
|
||||
eq(credentialMember.userId, userId),
|
||||
eq(credentialMember.status, 'active'),
|
||||
eq(credentialMember.role, 'admin'),
|
||||
eq(credential.workspaceId, workspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!membership) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Admin access required on the target credential' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
|
||||
await db
|
||||
.delete(pendingCredentialDraft)
|
||||
.where(
|
||||
and(eq(pendingCredentialDraft.userId, userId), lt(pendingCredentialDraft.expiresAt, now))
|
||||
)
|
||||
|
||||
await db
|
||||
.insert(pendingCredentialDraft)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
workspaceId,
|
||||
providerId,
|
||||
displayName,
|
||||
description: description || null,
|
||||
credentialId: credentialId || null,
|
||||
expiresAt: new Date(now.getTime() + DRAFT_TTL_MS),
|
||||
createdAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
pendingCredentialDraft.userId,
|
||||
pendingCredentialDraft.providerId,
|
||||
pendingCredentialDraft.workspaceId,
|
||||
],
|
||||
set: {
|
||||
displayName,
|
||||
description: description || null,
|
||||
credentialId: credentialId || null,
|
||||
expiresAt: new Date(now.getTime() + DRAFT_TTL_MS),
|
||||
createdAt: now,
|
||||
},
|
||||
})
|
||||
|
||||
logger.info('Credential draft saved', {
|
||||
userId,
|
||||
workspaceId,
|
||||
providerId,
|
||||
displayName,
|
||||
credentialId: credentialId || null,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error('Failed to save credential draft', { error })
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
120
apps/sim/app/api/credentials/memberships/route.ts
Normal file
120
apps/sim/app/api/credentials/memberships/route.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { db } from '@sim/db'
|
||||
import { credential, credentialMember } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
|
||||
const logger = createLogger('CredentialMembershipsAPI')
|
||||
|
||||
const leaveCredentialSchema = z.object({
|
||||
credentialId: z.string().min(1),
|
||||
})
|
||||
|
||||
export async function GET() {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const memberships = await db
|
||||
.select({
|
||||
membershipId: credentialMember.id,
|
||||
credentialId: credential.id,
|
||||
workspaceId: credential.workspaceId,
|
||||
type: credential.type,
|
||||
displayName: credential.displayName,
|
||||
providerId: credential.providerId,
|
||||
role: credentialMember.role,
|
||||
status: credentialMember.status,
|
||||
joinedAt: credentialMember.joinedAt,
|
||||
})
|
||||
.from(credentialMember)
|
||||
.innerJoin(credential, eq(credentialMember.credentialId, credential.id))
|
||||
.where(eq(credentialMember.userId, session.user.id))
|
||||
|
||||
return NextResponse.json({ memberships }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error('Failed to list credential memberships', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parseResult = leaveCredentialSchema.safeParse({
|
||||
credentialId: new URL(request.url).searchParams.get('credentialId'),
|
||||
})
|
||||
if (!parseResult.success) {
|
||||
return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const { credentialId } = parseResult.data
|
||||
const [membership] = await db
|
||||
.select()
|
||||
.from(credentialMember)
|
||||
.where(
|
||||
and(
|
||||
eq(credentialMember.credentialId, credentialId),
|
||||
eq(credentialMember.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!membership) {
|
||||
return NextResponse.json({ error: 'Membership not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (membership.status !== 'active') {
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
}
|
||||
|
||||
const revoked = await db.transaction(async (tx) => {
|
||||
if (membership.role === 'admin') {
|
||||
const activeAdmins = await tx
|
||||
.select({ id: credentialMember.id })
|
||||
.from(credentialMember)
|
||||
.where(
|
||||
and(
|
||||
eq(credentialMember.credentialId, credentialId),
|
||||
eq(credentialMember.role, 'admin'),
|
||||
eq(credentialMember.status, 'active')
|
||||
)
|
||||
)
|
||||
|
||||
if (activeAdmins.length <= 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(credentialMember)
|
||||
.set({
|
||||
status: 'revoked',
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(credentialMember.id, membership.id))
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (!revoked) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot leave credential as the last active admin' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error('Failed to leave credential', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
520
apps/sim/app/api/credentials/route.ts
Normal file
520
apps/sim/app/api/credentials/route.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
import { db } from '@sim/db'
|
||||
import { account, credential, credentialMember, workspace } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getWorkspaceMemberUserIds } from '@/lib/credentials/environment'
|
||||
import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth'
|
||||
import { getServiceConfigByProviderId } from '@/lib/oauth'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
import { isValidEnvVarName } from '@/executor/constants'
|
||||
|
||||
const logger = createLogger('CredentialsAPI')
|
||||
|
||||
const credentialTypeSchema = z.enum(['oauth', 'env_workspace', 'env_personal'])
|
||||
|
||||
function normalizeEnvKeyInput(raw: string): string {
|
||||
const trimmed = raw.trim()
|
||||
const wrappedMatch = /^\{\{\s*([A-Za-z0-9_]+)\s*\}\}$/.exec(trimmed)
|
||||
return wrappedMatch ? wrappedMatch[1] : trimmed
|
||||
}
|
||||
|
||||
const listCredentialsSchema = z.object({
|
||||
workspaceId: z.string().uuid('Workspace ID must be a valid UUID'),
|
||||
type: credentialTypeSchema.optional(),
|
||||
providerId: z.string().optional(),
|
||||
credentialId: z.string().optional(),
|
||||
})
|
||||
|
||||
const createCredentialSchema = z
|
||||
.object({
|
||||
workspaceId: z.string().uuid('Workspace ID must be a valid UUID'),
|
||||
type: credentialTypeSchema,
|
||||
displayName: z.string().trim().min(1).max(255).optional(),
|
||||
description: z.string().trim().max(500).optional(),
|
||||
providerId: z.string().trim().min(1).optional(),
|
||||
accountId: z.string().trim().min(1).optional(),
|
||||
envKey: z.string().trim().min(1).optional(),
|
||||
envOwnerUserId: z.string().trim().min(1).optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.type === 'oauth') {
|
||||
if (!data.accountId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'accountId is required for oauth credentials',
|
||||
path: ['accountId'],
|
||||
})
|
||||
}
|
||||
if (!data.providerId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'providerId is required for oauth credentials',
|
||||
path: ['providerId'],
|
||||
})
|
||||
}
|
||||
if (!data.displayName) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'displayName is required for oauth credentials',
|
||||
path: ['displayName'],
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedEnvKey = data.envKey ? normalizeEnvKeyInput(data.envKey) : ''
|
||||
if (!normalizedEnvKey) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'envKey is required for env credentials',
|
||||
path: ['envKey'],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!isValidEnvVarName(normalizedEnvKey)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'envKey must contain only letters, numbers, and underscores',
|
||||
path: ['envKey'],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
interface ExistingCredentialSourceParams {
|
||||
workspaceId: string
|
||||
type: 'oauth' | 'env_workspace' | 'env_personal'
|
||||
accountId?: string | null
|
||||
envKey?: string | null
|
||||
envOwnerUserId?: string | null
|
||||
}
|
||||
|
||||
async function findExistingCredentialBySource(params: ExistingCredentialSourceParams) {
|
||||
const { workspaceId, type, accountId, envKey, envOwnerUserId } = params
|
||||
|
||||
if (type === 'oauth' && accountId) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(credential)
|
||||
.where(
|
||||
and(
|
||||
eq(credential.workspaceId, workspaceId),
|
||||
eq(credential.type, 'oauth'),
|
||||
eq(credential.accountId, accountId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
return row ?? null
|
||||
}
|
||||
|
||||
if (type === 'env_workspace' && envKey) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(credential)
|
||||
.where(
|
||||
and(
|
||||
eq(credential.workspaceId, workspaceId),
|
||||
eq(credential.type, 'env_workspace'),
|
||||
eq(credential.envKey, envKey)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
return row ?? null
|
||||
}
|
||||
|
||||
if (type === 'env_personal' && envKey && envOwnerUserId) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(credential)
|
||||
.where(
|
||||
and(
|
||||
eq(credential.workspaceId, workspaceId),
|
||||
eq(credential.type, 'env_personal'),
|
||||
eq(credential.envKey, envKey),
|
||||
eq(credential.envOwnerUserId, envOwnerUserId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
return row ?? null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const rawWorkspaceId = searchParams.get('workspaceId')
|
||||
const rawType = searchParams.get('type')
|
||||
const rawProviderId = searchParams.get('providerId')
|
||||
const rawCredentialId = searchParams.get('credentialId')
|
||||
const parseResult = listCredentialsSchema.safeParse({
|
||||
workspaceId: rawWorkspaceId?.trim(),
|
||||
type: rawType?.trim() || undefined,
|
||||
providerId: rawProviderId?.trim() || undefined,
|
||||
credentialId: rawCredentialId?.trim() || undefined,
|
||||
})
|
||||
|
||||
if (!parseResult.success) {
|
||||
logger.warn(`[${requestId}] Invalid credential list request`, {
|
||||
workspaceId: rawWorkspaceId,
|
||||
type: rawType,
|
||||
providerId: rawProviderId,
|
||||
errors: parseResult.error.errors,
|
||||
})
|
||||
return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const { workspaceId, type, providerId, credentialId: lookupCredentialId } = parseResult.data
|
||||
const workspaceAccess = await checkWorkspaceAccess(workspaceId, session.user.id)
|
||||
|
||||
if (!workspaceAccess.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (lookupCredentialId) {
|
||||
let [row] = await db
|
||||
.select({
|
||||
id: credential.id,
|
||||
displayName: credential.displayName,
|
||||
type: credential.type,
|
||||
providerId: credential.providerId,
|
||||
})
|
||||
.from(credential)
|
||||
.where(and(eq(credential.id, lookupCredentialId), eq(credential.workspaceId, workspaceId)))
|
||||
.limit(1)
|
||||
|
||||
if (!row) {
|
||||
;[row] = await db
|
||||
.select({
|
||||
id: credential.id,
|
||||
displayName: credential.displayName,
|
||||
type: credential.type,
|
||||
providerId: credential.providerId,
|
||||
})
|
||||
.from(credential)
|
||||
.where(
|
||||
and(
|
||||
eq(credential.accountId, lookupCredentialId),
|
||||
eq(credential.workspaceId, workspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
}
|
||||
|
||||
return NextResponse.json({ credential: row ?? null })
|
||||
}
|
||||
|
||||
if (!type || type === 'oauth') {
|
||||
await syncWorkspaceOAuthCredentialsForUser({ workspaceId, userId: session.user.id })
|
||||
}
|
||||
|
||||
const whereClauses = [eq(credential.workspaceId, workspaceId)]
|
||||
|
||||
if (type) {
|
||||
whereClauses.push(eq(credential.type, type))
|
||||
}
|
||||
if (providerId) {
|
||||
whereClauses.push(eq(credential.providerId, providerId))
|
||||
}
|
||||
|
||||
const credentials = await db
|
||||
.select({
|
||||
id: credential.id,
|
||||
workspaceId: credential.workspaceId,
|
||||
type: credential.type,
|
||||
displayName: credential.displayName,
|
||||
description: credential.description,
|
||||
providerId: credential.providerId,
|
||||
accountId: credential.accountId,
|
||||
envKey: credential.envKey,
|
||||
envOwnerUserId: credential.envOwnerUserId,
|
||||
createdBy: credential.createdBy,
|
||||
createdAt: credential.createdAt,
|
||||
updatedAt: credential.updatedAt,
|
||||
role: credentialMember.role,
|
||||
})
|
||||
.from(credential)
|
||||
.innerJoin(
|
||||
credentialMember,
|
||||
and(
|
||||
eq(credentialMember.credentialId, credential.id),
|
||||
eq(credentialMember.userId, session.user.id),
|
||||
eq(credentialMember.status, 'active')
|
||||
)
|
||||
)
|
||||
.where(and(...whereClauses))
|
||||
|
||||
return NextResponse.json({ credentials })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to list credentials`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const parseResult = createCredentialSchema.safeParse(body)
|
||||
|
||||
if (!parseResult.success) {
|
||||
return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const {
|
||||
workspaceId,
|
||||
type,
|
||||
displayName,
|
||||
description,
|
||||
providerId,
|
||||
accountId,
|
||||
envKey,
|
||||
envOwnerUserId,
|
||||
} = parseResult.data
|
||||
|
||||
const workspaceAccess = await checkWorkspaceAccess(workspaceId, session.user.id)
|
||||
if (!workspaceAccess.canWrite) {
|
||||
return NextResponse.json({ error: 'Write permission required' }, { status: 403 })
|
||||
}
|
||||
|
||||
let resolvedDisplayName = displayName?.trim() ?? ''
|
||||
const resolvedDescription = description?.trim() || null
|
||||
let resolvedProviderId: string | null = providerId ?? null
|
||||
let resolvedAccountId: string | null = accountId ?? null
|
||||
const resolvedEnvKey: string | null = envKey ? normalizeEnvKeyInput(envKey) : null
|
||||
let resolvedEnvOwnerUserId: string | null = null
|
||||
|
||||
if (type === 'oauth') {
|
||||
const [accountRow] = await db
|
||||
.select({
|
||||
id: account.id,
|
||||
userId: account.userId,
|
||||
providerId: account.providerId,
|
||||
accountId: account.accountId,
|
||||
})
|
||||
.from(account)
|
||||
.where(eq(account.id, accountId!))
|
||||
.limit(1)
|
||||
|
||||
if (!accountRow) {
|
||||
return NextResponse.json({ error: 'OAuth account not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (accountRow.userId !== session.user.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only account owners can create oauth credentials for an account' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
if (providerId !== accountRow.providerId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'providerId does not match the selected OAuth account' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
if (!resolvedDisplayName) {
|
||||
resolvedDisplayName =
|
||||
getServiceConfigByProviderId(accountRow.providerId)?.name || accountRow.providerId
|
||||
}
|
||||
} else if (type === 'env_personal') {
|
||||
resolvedEnvOwnerUserId = envOwnerUserId ?? session.user.id
|
||||
if (resolvedEnvOwnerUserId !== session.user.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only the current user can create personal env credentials for themselves' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
resolvedProviderId = null
|
||||
resolvedAccountId = null
|
||||
resolvedDisplayName = resolvedEnvKey || ''
|
||||
} else {
|
||||
resolvedProviderId = null
|
||||
resolvedAccountId = null
|
||||
resolvedEnvOwnerUserId = null
|
||||
resolvedDisplayName = resolvedEnvKey || ''
|
||||
}
|
||||
|
||||
if (!resolvedDisplayName) {
|
||||
return NextResponse.json({ error: 'Display name is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const existingCredential = await findExistingCredentialBySource({
|
||||
workspaceId,
|
||||
type,
|
||||
accountId: resolvedAccountId,
|
||||
envKey: resolvedEnvKey,
|
||||
envOwnerUserId: resolvedEnvOwnerUserId,
|
||||
})
|
||||
|
||||
if (existingCredential) {
|
||||
const [membership] = await db
|
||||
.select({
|
||||
id: credentialMember.id,
|
||||
status: credentialMember.status,
|
||||
role: credentialMember.role,
|
||||
})
|
||||
.from(credentialMember)
|
||||
.where(
|
||||
and(
|
||||
eq(credentialMember.credentialId, existingCredential.id),
|
||||
eq(credentialMember.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!membership || membership.status !== 'active') {
|
||||
return NextResponse.json(
|
||||
{ error: 'A credential with this source already exists in this workspace' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
const canUpdateExistingCredential = membership.role === 'admin'
|
||||
const shouldUpdateDisplayName =
|
||||
type === 'oauth' &&
|
||||
resolvedDisplayName &&
|
||||
resolvedDisplayName !== existingCredential.displayName
|
||||
const shouldUpdateDescription =
|
||||
typeof description !== 'undefined' &&
|
||||
(existingCredential.description ?? null) !== resolvedDescription
|
||||
|
||||
if (canUpdateExistingCredential && (shouldUpdateDisplayName || shouldUpdateDescription)) {
|
||||
await db
|
||||
.update(credential)
|
||||
.set({
|
||||
...(shouldUpdateDisplayName ? { displayName: resolvedDisplayName } : {}),
|
||||
...(shouldUpdateDescription ? { description: resolvedDescription } : {}),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(credential.id, existingCredential.id))
|
||||
|
||||
const [updatedCredential] = await db
|
||||
.select()
|
||||
.from(credential)
|
||||
.where(eq(credential.id, existingCredential.id))
|
||||
.limit(1)
|
||||
|
||||
return NextResponse.json(
|
||||
{ credential: updatedCredential ?? existingCredential },
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ credential: existingCredential }, { status: 200 })
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const credentialId = crypto.randomUUID()
|
||||
const [workspaceRow] = await db
|
||||
.select({ ownerId: workspace.ownerId })
|
||||
.from(workspace)
|
||||
.where(eq(workspace.id, workspaceId))
|
||||
.limit(1)
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(credential).values({
|
||||
id: credentialId,
|
||||
workspaceId,
|
||||
type,
|
||||
displayName: resolvedDisplayName,
|
||||
description: resolvedDescription,
|
||||
providerId: resolvedProviderId,
|
||||
accountId: resolvedAccountId,
|
||||
envKey: resolvedEnvKey,
|
||||
envOwnerUserId: resolvedEnvOwnerUserId,
|
||||
createdBy: session.user.id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
if (type === 'env_workspace' && workspaceRow?.ownerId) {
|
||||
const workspaceUserIds = await getWorkspaceMemberUserIds(workspaceId)
|
||||
if (workspaceUserIds.length > 0) {
|
||||
for (const memberUserId of workspaceUserIds) {
|
||||
await tx.insert(credentialMember).values({
|
||||
id: crypto.randomUUID(),
|
||||
credentialId,
|
||||
userId: memberUserId,
|
||||
role:
|
||||
memberUserId === workspaceRow.ownerId || memberUserId === session.user.id
|
||||
? 'admin'
|
||||
: 'member',
|
||||
status: 'active',
|
||||
joinedAt: now,
|
||||
invitedBy: session.user.id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await tx.insert(credentialMember).values({
|
||||
id: crypto.randomUUID(),
|
||||
credentialId,
|
||||
userId: session.user.id,
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
joinedAt: now,
|
||||
invitedBy: session.user.id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const [created] = await db
|
||||
.select()
|
||||
.from(credential)
|
||||
.where(eq(credential.id, credentialId))
|
||||
.limit(1)
|
||||
|
||||
return NextResponse.json({ credential: created }, { status: 201 })
|
||||
} catch (error: any) {
|
||||
if (error?.code === '23505') {
|
||||
return NextResponse.json(
|
||||
{ error: 'A credential with this source already exists' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
if (error?.code === '23503') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid credential reference or membership target' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
if (error?.code === '23514') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Credential source data failed validation checks' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error(`[${requestId}] Credential create failure details`, {
|
||||
code: error?.code,
|
||||
detail: error?.detail,
|
||||
constraint: error?.constraint,
|
||||
table: error?.table,
|
||||
message: error?.message,
|
||||
})
|
||||
logger.error(`[${requestId}] Failed to create credential`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,27 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq, or } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { verifyCronAuth } from '@/lib/auth/internal'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('TeamsSubscriptionRenewal')
|
||||
|
||||
async function getCredentialOwnerUserId(credentialId: string): Promise<string | null> {
|
||||
async function getCredentialOwner(
|
||||
credentialId: string
|
||||
): Promise<{ userId: string; accountId: string } | null> {
|
||||
const resolved = await resolveOAuthAccountId(credentialId)
|
||||
if (!resolved) {
|
||||
logger.error(`Failed to resolve OAuth account for credential ${credentialId}`)
|
||||
return null
|
||||
}
|
||||
const [credentialRecord] = await db
|
||||
.select({ userId: account.userId })
|
||||
.from(account)
|
||||
.where(eq(account.id, credentialId))
|
||||
.where(eq(account.id, resolved.accountId))
|
||||
.limit(1)
|
||||
|
||||
return credentialRecord?.userId ?? null
|
||||
return credentialRecord
|
||||
? { userId: credentialRecord.userId, accountId: resolved.accountId }
|
||||
: null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,8 +97,8 @@ export async function GET(request: NextRequest) {
|
||||
continue
|
||||
}
|
||||
|
||||
const credentialOwnerUserId = await getCredentialOwnerUserId(credentialId)
|
||||
if (!credentialOwnerUserId) {
|
||||
const credentialOwner = await getCredentialOwner(credentialId)
|
||||
if (!credentialOwner) {
|
||||
logger.error(`Credential owner not found for credential ${credentialId}`)
|
||||
totalFailed++
|
||||
continue
|
||||
@@ -97,8 +106,8 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// Get fresh access token
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credentialId,
|
||||
credentialOwnerUserId,
|
||||
credentialOwner.accountId,
|
||||
credentialOwner.userId,
|
||||
`renewal-${webhook.id}`
|
||||
)
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { syncPersonalEnvCredentialsForUser } from '@/lib/credentials/environment'
|
||||
import type { EnvironmentVariable } from '@/stores/settings/environment'
|
||||
|
||||
const logger = createLogger('EnvironmentAPI')
|
||||
@@ -53,6 +55,22 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
await syncPersonalEnvCredentialsForUser({
|
||||
userId: session.user.id,
|
||||
envKeys: Object.keys(variables),
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.ENVIRONMENT_UPDATED,
|
||||
resourceType: AuditResourceType.ENVIRONMENT,
|
||||
description: 'Updated global environment variables',
|
||||
metadata: { variableCount: Object.keys(variables).length },
|
||||
request: req,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (validationError) {
|
||||
if (validationError instanceof z.ZodError) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
createMockRequest,
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockHybridAuth,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
@@ -28,13 +29,12 @@ function setupFileApiMocks(
|
||||
authMocks.setUnauthenticated()
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth } = mockHybridAuth()
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createMockRequest,
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockHybridAuth,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
@@ -34,13 +35,12 @@ function setupFileApiMocks(
|
||||
authMocks.setUnauthenticated()
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckInternalAuth } = mockHybridAuth()
|
||||
mockCheckInternalAuth.mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { mockAuth, mockCryptoUuid, mockUuid, setupCommonApiMocks } from '@sim/testing'
|
||||
import {
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockHybridAuth,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -28,13 +34,12 @@ function setupFileApiMocks(
|
||||
authMocks.setUnauthenticated()
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckHybridAuth } = mockHybridAuth()
|
||||
mockCheckHybridAuth.mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
defaultMockUser,
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockHybridAuth,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
@@ -54,12 +55,11 @@ describe('File Serve API Route', () => {
|
||||
withUploadUtils: true,
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: serveAuthMock } = mockHybridAuth()
|
||||
serveAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
@@ -164,12 +164,11 @@ describe('File Serve API Route', () => {
|
||||
findLocalFile: vi.fn().mockReturnValue('/test/uploads/nested/path/file.txt'),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: serveAuthMock } = mockHybridAuth()
|
||||
serveAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
@@ -225,12 +224,11 @@ describe('File Serve API Route', () => {
|
||||
USE_BLOB_STORAGE: false,
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: serveAuthMock } = mockHybridAuth()
|
||||
serveAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
@@ -290,12 +288,11 @@ describe('File Serve API Route', () => {
|
||||
readFile: vi.fn().mockRejectedValue(new Error('ENOENT: no such file or directory')),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: serveAuthMock } = mockHybridAuth()
|
||||
serveAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(false), // File not found = no access
|
||||
@@ -349,12 +346,11 @@ describe('File Serve API Route', () => {
|
||||
|
||||
for (const test of contentTypeTests) {
|
||||
it(`should serve ${test.ext} file with correct content type`, async () => {
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: ctAuthMock } = mockHybridAuth()
|
||||
ctAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { mockAuth, mockCryptoUuid, mockUuid, setupCommonApiMocks } from '@sim/testing'
|
||||
import {
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockHybridAuth,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -27,13 +33,12 @@ function setupFileApiMocks(
|
||||
authMocks.setUnauthenticated()
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckHybridAuth } = mockHybridAuth()
|
||||
mockCheckHybridAuth.mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflow, workflowFolder } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { and, eq, isNull, min } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
@@ -37,7 +37,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
|
||||
logger.info(`[${requestId}] Duplicating folder ${sourceFolderId} for user ${session.user.id}`)
|
||||
|
||||
// Verify the source folder exists
|
||||
const sourceFolder = await db
|
||||
.select()
|
||||
.from(workflowFolder)
|
||||
@@ -48,7 +47,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
throw new Error('Source folder not found')
|
||||
}
|
||||
|
||||
// Check if user has permission to access the source folder
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
@@ -61,26 +59,51 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
|
||||
const targetWorkspaceId = workspaceId || sourceFolder.workspaceId
|
||||
|
||||
// Step 1: Duplicate folder structure
|
||||
const { newFolderId, folderMapping } = await db.transaction(async (tx) => {
|
||||
const newFolderId = crypto.randomUUID()
|
||||
const now = new Date()
|
||||
const targetParentId = parentId ?? sourceFolder.parentId
|
||||
|
||||
const folderParentCondition = targetParentId
|
||||
? eq(workflowFolder.parentId, targetParentId)
|
||||
: isNull(workflowFolder.parentId)
|
||||
const workflowParentCondition = targetParentId
|
||||
? eq(workflow.folderId, targetParentId)
|
||||
: isNull(workflow.folderId)
|
||||
|
||||
const [[folderResult], [workflowResult]] = await Promise.all([
|
||||
tx
|
||||
.select({ minSortOrder: min(workflowFolder.sortOrder) })
|
||||
.from(workflowFolder)
|
||||
.where(and(eq(workflowFolder.workspaceId, targetWorkspaceId), folderParentCondition)),
|
||||
tx
|
||||
.select({ minSortOrder: min(workflow.sortOrder) })
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.workspaceId, targetWorkspaceId), workflowParentCondition)),
|
||||
])
|
||||
|
||||
const minSortOrder = [folderResult?.minSortOrder, workflowResult?.minSortOrder].reduce<
|
||||
number | null
|
||||
>((currentMin, candidate) => {
|
||||
if (candidate == null) return currentMin
|
||||
if (currentMin == null) return candidate
|
||||
return Math.min(currentMin, candidate)
|
||||
}, null)
|
||||
const sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
|
||||
|
||||
// Create the new root folder
|
||||
await tx.insert(workflowFolder).values({
|
||||
id: newFolderId,
|
||||
userId: session.user.id,
|
||||
workspaceId: targetWorkspaceId,
|
||||
name,
|
||||
color: color || sourceFolder.color,
|
||||
parentId: parentId || sourceFolder.parentId,
|
||||
sortOrder: sourceFolder.sortOrder,
|
||||
parentId: targetParentId,
|
||||
sortOrder,
|
||||
isExpanded: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
// Recursively duplicate child folders
|
||||
const folderMapping = new Map<string, string>([[sourceFolderId, newFolderId]])
|
||||
await duplicateFolderStructure(
|
||||
tx,
|
||||
@@ -96,7 +119,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
return { newFolderId, folderMapping }
|
||||
})
|
||||
|
||||
// Step 2: Duplicate workflows
|
||||
const workflowStats = await duplicateWorkflowsInFolderTree(
|
||||
sourceFolder.workspaceId,
|
||||
targetWorkspaceId,
|
||||
@@ -126,6 +148,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: name,
|
||||
description: `Duplicated folder "${sourceFolder.name}" as "${name}"`,
|
||||
metadata: {
|
||||
sourceId: sourceFolder.id,
|
||||
affected: { workflows: workflowStats.succeeded, folders: folderMapping.size },
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -173,7 +199,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to recursively duplicate folder structure
|
||||
async function duplicateFolderStructure(
|
||||
tx: any,
|
||||
sourceFolderId: string,
|
||||
@@ -184,7 +209,6 @@ async function duplicateFolderStructure(
|
||||
timestamp: Date,
|
||||
folderMapping: Map<string, string>
|
||||
): Promise<void> {
|
||||
// Get all child folders
|
||||
const childFolders = await tx
|
||||
.select()
|
||||
.from(workflowFolder)
|
||||
@@ -195,7 +219,6 @@ async function duplicateFolderStructure(
|
||||
)
|
||||
)
|
||||
|
||||
// Create each child folder and recurse
|
||||
for (const childFolder of childFolders) {
|
||||
const newChildFolderId = crypto.randomUUID()
|
||||
folderMapping.set(childFolder.id, newChildFolderId)
|
||||
@@ -213,7 +236,6 @@ async function duplicateFolderStructure(
|
||||
updatedAt: timestamp,
|
||||
})
|
||||
|
||||
// Recurse for this child's children
|
||||
await duplicateFolderStructure(
|
||||
tx,
|
||||
childFolder.id,
|
||||
@@ -227,7 +249,6 @@ async function duplicateFolderStructure(
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to duplicate all workflows in a folder tree
|
||||
async function duplicateWorkflowsInFolderTree(
|
||||
sourceWorkspaceId: string,
|
||||
targetWorkspaceId: string,
|
||||
@@ -237,9 +258,7 @@ async function duplicateWorkflowsInFolderTree(
|
||||
): Promise<{ total: number; succeeded: number; failed: number }> {
|
||||
const stats = { total: 0, succeeded: 0, failed: 0 }
|
||||
|
||||
// Process each folder in the mapping
|
||||
for (const [oldFolderId, newFolderId] of folderMapping.entries()) {
|
||||
// Get workflows in this folder
|
||||
const workflowsInFolder = await db
|
||||
.select()
|
||||
.from(workflow)
|
||||
@@ -247,7 +266,6 @@ async function duplicateWorkflowsInFolderTree(
|
||||
|
||||
stats.total += workflowsInFolder.length
|
||||
|
||||
// Duplicate each workflow
|
||||
for (const sourceWorkflow of workflowsInFolder) {
|
||||
try {
|
||||
await duplicateWorkflow({
|
||||
|
||||
@@ -178,6 +178,12 @@ export async function DELETE(
|
||||
resourceId: id,
|
||||
resourceName: existingFolder.name,
|
||||
description: `Deleted folder "${existingFolder.name}"`,
|
||||
metadata: {
|
||||
affected: {
|
||||
workflows: deletionStats.workflows,
|
||||
subfolders: deletionStats.folders - 1,
|
||||
},
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -10,9 +10,14 @@ import {
|
||||
mockConsoleLogger,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
import { drizzleOrmMock } from '@sim/testing/mocks'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/lib/audit/log', () => auditMock)
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
...drizzleOrmMock,
|
||||
min: vi.fn((field) => ({ type: 'min', field })),
|
||||
}))
|
||||
|
||||
interface CapturedFolderValues {
|
||||
name?: string
|
||||
@@ -24,29 +29,35 @@ interface CapturedFolderValues {
|
||||
}
|
||||
|
||||
function createMockTransaction(mockData: {
|
||||
selectData?: Array<{ id: string; [key: string]: unknown }>
|
||||
selectResults?: Array<Array<{ [key: string]: unknown }>>
|
||||
insertResult?: Array<{ id: string; [key: string]: unknown }>
|
||||
onInsertValues?: (values: CapturedFolderValues) => void
|
||||
}) {
|
||||
const { selectData = [], insertResult = [] } = mockData
|
||||
return vi.fn().mockImplementation(async (callback: (tx: unknown) => Promise<unknown>) => {
|
||||
const { selectResults = [[], []], insertResult = [], onInsertValues } = mockData
|
||||
return async (callback: (tx: unknown) => Promise<unknown>) => {
|
||||
const where = vi.fn()
|
||||
for (const result of selectResults) {
|
||||
where.mockReturnValueOnce(result)
|
||||
}
|
||||
where.mockReturnValue([])
|
||||
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue(selectData),
|
||||
}),
|
||||
}),
|
||||
where,
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockReturnValue(insertResult),
|
||||
values: vi.fn().mockImplementation((values: CapturedFolderValues) => {
|
||||
onInsertValues?.(values)
|
||||
return {
|
||||
returning: vi.fn().mockReturnValue(insertResult),
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe('Folders API Route', () => {
|
||||
@@ -257,25 +268,12 @@ describe('Folders API Route', () => {
|
||||
it('should create a new folder successfully', async () => {
|
||||
mockAuthenticatedUser()
|
||||
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]), // No existing folders
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockReturnValue([mockFolders[0]]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
insertResult: [mockFolders[0]],
|
||||
})
|
||||
)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'New Test Folder',
|
||||
@@ -285,12 +283,11 @@ describe('Folders API Route', () => {
|
||||
|
||||
const { POST } = await import('@/app/api/folders/route')
|
||||
const response = await POST(req)
|
||||
const responseBody = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const data = await response.json()
|
||||
expect(data).toHaveProperty('folder')
|
||||
expect(data.folder).toMatchObject({
|
||||
expect(responseBody).toHaveProperty('folder')
|
||||
expect(responseBody.folder).toMatchObject({
|
||||
id: 'folder-1',
|
||||
name: 'Test Folder 1',
|
||||
workspaceId: 'workspace-123',
|
||||
@@ -299,26 +296,17 @@ describe('Folders API Route', () => {
|
||||
|
||||
it('should create folder with correct sort order', async () => {
|
||||
mockAuthenticatedUser()
|
||||
let capturedValues: CapturedFolderValues | null = null
|
||||
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([{ sortOrder: 5 }]), // Existing folder with sort order 5
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockReturnValue([{ ...mockFolders[0], sortOrder: 6 }]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[{ minSortOrder: 5 }], [{ minSortOrder: 2 }]],
|
||||
insertResult: [{ ...mockFolders[0], sortOrder: 1 }],
|
||||
onInsertValues: (values) => {
|
||||
capturedValues = values
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'New Test Folder',
|
||||
@@ -332,8 +320,10 @@ describe('Folders API Route', () => {
|
||||
|
||||
const data = await response.json()
|
||||
expect(data.folder).toMatchObject({
|
||||
sortOrder: 6,
|
||||
sortOrder: 1,
|
||||
})
|
||||
expect(capturedValues).not.toBeNull()
|
||||
expect(capturedValues!.sortOrder).toBe(1)
|
||||
})
|
||||
|
||||
it('should create subfolder with parent reference', async () => {
|
||||
@@ -341,7 +331,7 @@ describe('Folders API Route', () => {
|
||||
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectData: [], // No existing folders
|
||||
selectResults: [[], []],
|
||||
insertResult: [{ ...mockFolders[1] }],
|
||||
})
|
||||
)
|
||||
@@ -402,25 +392,12 @@ describe('Folders API Route', () => {
|
||||
mockAuthenticatedUser()
|
||||
mockGetUserEntityPermissions.mockResolvedValue('write') // Write permissions
|
||||
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]), // No existing folders
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockReturnValue([mockFolders[0]]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
insertResult: [mockFolders[0]],
|
||||
})
|
||||
)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'Test Folder',
|
||||
@@ -440,25 +417,12 @@ describe('Folders API Route', () => {
|
||||
mockAuthenticatedUser()
|
||||
mockGetUserEntityPermissions.mockResolvedValue('admin') // Admin permissions
|
||||
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]), // No existing folders
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockReturnValue([mockFolders[0]]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
insertResult: [mockFolders[0]],
|
||||
})
|
||||
)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'Test Folder',
|
||||
@@ -527,28 +491,15 @@ describe('Folders API Route', () => {
|
||||
|
||||
let capturedValues: CapturedFolderValues | null = null
|
||||
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockImplementation((values) => {
|
||||
capturedValues = values
|
||||
return {
|
||||
returning: vi.fn().mockReturnValue([mockFolders[0]]),
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
insertResult: [mockFolders[0]],
|
||||
onInsertValues: (values) => {
|
||||
capturedValues = values
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: ' Test Folder With Spaces ',
|
||||
@@ -567,28 +518,15 @@ describe('Folders API Route', () => {
|
||||
|
||||
let capturedValues: CapturedFolderValues | null = null
|
||||
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockImplementation((values) => {
|
||||
capturedValues = values
|
||||
return {
|
||||
returning: vi.fn().mockReturnValue([mockFolders[0]]),
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
insertResult: [mockFolders[0]],
|
||||
onInsertValues: (values) => {
|
||||
capturedValues = values
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'Test Folder',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflowFolder } from '@sim/db/schema'
|
||||
import { workflow, workflowFolder } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, asc, desc, eq, isNull } from 'drizzle-orm'
|
||||
import { and, asc, eq, isNull, min } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
@@ -87,19 +87,33 @@ export async function POST(request: NextRequest) {
|
||||
if (providedSortOrder !== undefined) {
|
||||
sortOrder = providedSortOrder
|
||||
} else {
|
||||
const existingFolders = await tx
|
||||
.select({ sortOrder: workflowFolder.sortOrder })
|
||||
.from(workflowFolder)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowFolder.workspaceId, workspaceId),
|
||||
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(workflowFolder.sortOrder))
|
||||
.limit(1)
|
||||
const folderParentCondition = parentId
|
||||
? eq(workflowFolder.parentId, parentId)
|
||||
: isNull(workflowFolder.parentId)
|
||||
const workflowParentCondition = parentId
|
||||
? eq(workflow.folderId, parentId)
|
||||
: isNull(workflow.folderId)
|
||||
|
||||
sortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0
|
||||
const [[folderResult], [workflowResult]] = await Promise.all([
|
||||
tx
|
||||
.select({ minSortOrder: min(workflowFolder.sortOrder) })
|
||||
.from(workflowFolder)
|
||||
.where(and(eq(workflowFolder.workspaceId, workspaceId), folderParentCondition)),
|
||||
tx
|
||||
.select({ minSortOrder: min(workflow.sortOrder) })
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.workspaceId, workspaceId), workflowParentCondition)),
|
||||
])
|
||||
|
||||
const minSortOrder = [folderResult?.minSortOrder, workflowResult?.minSortOrder].reduce<
|
||||
number | null
|
||||
>((currentMin, candidate) => {
|
||||
if (candidate == null) return currentMin
|
||||
if (currentMin == null) return candidate
|
||||
return Math.min(currentMin, candidate)
|
||||
}, null)
|
||||
|
||||
sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
|
||||
}
|
||||
|
||||
const [folder] = await tx
|
||||
|
||||
@@ -58,8 +58,6 @@ export async function POST(
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
logger.debug(`[${requestId}] Processing form submission for identifier: ${identifier}`)
|
||||
|
||||
let parsedBody
|
||||
try {
|
||||
const rawBody = await request.json()
|
||||
@@ -300,8 +298,6 @@ export async function GET(
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
logger.debug(`[${requestId}] Fetching form info for identifier: ${identifier}`)
|
||||
|
||||
const deploymentResult = await db
|
||||
.select({
|
||||
id: form.id,
|
||||
|
||||
@@ -211,7 +211,7 @@ describe('Function Execute API Route', () => {
|
||||
|
||||
it.concurrent('should block SSRF attacks through secure fetch wrapper', async () => {
|
||||
expect(validateProxyUrl('http://169.254.169.254/latest/meta-data/').isValid).toBe(false)
|
||||
expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(false)
|
||||
expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(true)
|
||||
expect(validateProxyUrl('http://192.168.1.1/config').isValid).toBe(false)
|
||||
expect(validateProxyUrl('http://10.0.0.1/internal').isValid).toBe(false)
|
||||
})
|
||||
|
||||
@@ -77,8 +77,6 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`[${requestId}] Help request includes ${images.length} images`)
|
||||
|
||||
const userId = session.user.id
|
||||
let emailText = `
|
||||
Type: ${type}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user