Compare commits

..

20 Commits

Author SHA1 Message Date
Waleed Latif
98b4586df2 refactor(webhooks): remove remaining any types from deploy.ts
Replace 3 `catch (error: any)` with `catch (error: unknown)` and
1 `Record<string, any>` with `Record<string, unknown>`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 23:39:01 -07:00
Waleed Latif
220aa91dab refactor(webhooks): standardize logger names and remove any types from providers
- Standardize logger names to WebhookProvider:X pattern across 6 providers
  (fathom, gmail, imap, lemlist, outlook, rss)
- Replace all `any` types in airtable handler with proper types:
  - Add AirtableTableChanges interface for API response typing
  - Change function params from `any` to `Record<string, unknown>`
  - Change AirtableChange fields from Record<string, any> to Record<string, unknown>
  - Change all catch blocks from `error: any` to `error: unknown`
  - Change input object from `any` to `Record<string, unknown>`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 12:36:05 -07:00
Waleed Latif
7afed0d589 fix(webhooks): replace crypto.randomUUID() with generateId() in ashby handler
Per project coding standards, use generateId() from @/lib/core/utils/uuid
instead of crypto.randomUUID() directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 12:28:45 -07:00
Waleed Latif
78e6de5c12 fix(webhooks): remove unintended rejectUnauthorized field from IMAP polling config
The refactored IMAP handler added a rejectUnauthorized field that was not
present in the original configureImapPolling function. This would default
to true for all existing IMAP webhooks, potentially breaking connections
to servers with self-signed certificates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 12:13:21 -07:00
Waleed Latif
1478de1332 fix(webhooks): validate auth token is set when requireAuth is enabled at deploy time
Rejects deployment with a clear error message if a generic webhook trigger
has requireAuth enabled but no authentication token configured, rather than
letting requests fail with 401 at runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 12:09:01 -07:00
Waleed Latif
60610b7351 fix(webhooks): remove duplicate generic file processing from webhook-execution
The generic provider's processInputFiles handler already handles file[] field
processing via the handler.processInputFiles call. The hardcoded block from
staging was incorrectly preserved during rebase, causing double processing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 11:55:21 -07:00
Waleed Latif
8bcf450d22 fix(webhooks): remove unused imports from utils.server.ts after rebase
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 11:41:01 -07:00
Waleed Latif
3ad355e95a fix(webhooks): fix attio build error, restore imap field, remove demarcation comments
- Cast `body` to `Record<string, unknown>` in attio formatInput to fix
  type error with extractor functions
- Restore `rejectUnauthorized` field in imap configurePolling for parity
- Remove `// ---` section demarcation comments from route.ts and airtable.ts
- Update add-trigger skill to reflect handler-based architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 11:40:38 -07:00
Waleed Latif
0d2f78bc8f refactor(webhooks): decompose provider-subscriptions into handler registry pattern
Move all provider-specific subscription create/delete logic from the monolithic
provider-subscriptions.ts into individual provider handler files via new
createSubscription/deleteSubscription methods on WebhookProviderHandler.

Replace the two massive if-else dispatch chains (11 branches each) with simple
registry lookups via getProviderHandler(). provider-subscriptions.ts reduced
from 2,337 lines to 128 lines (orchestration only).

Also migrate polling configuration (gmail, outlook, rss, imap) into provider
handlers via configurePolling() method, and challenge/verification handling
(slack, whatsapp, teams) via handleChallenge() method. Delete polling-config.ts.

Create new handler files for fathom and lemlist providers. Extract shared
subscription utilities into subscription-utils.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 11:40:38 -07:00
Waleed Latif
ced7d1478f refactor(webhooks): decompose formatWebhookInput into per-provider formatInput methods
Move all provider-specific input formatting from the monolithic formatWebhookInput
switch statement into each provider's handler file. Delete formatWebhookInput and
all its helper functions (fetchWithDNSPinning, formatTeamsGraphNotification, Slack
file helpers, convertSquareBracketsToTwiML) from utils.server.ts. Create new handler
files for gmail, outlook, rss, imap, and calendly providers. Update webhook-execution.ts
to use handler.formatInput as the primary path with raw body passthrough as fallback.

utils.server.ts reduced from ~1600 lines to ~370 lines containing only credential-sync
functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 11:39:46 -07:00
Waleed Latif
adf13bcbb3 refactor(webhooks): extract polling config functions into polling-config.ts
Move configureGmailPolling, configureOutlookPolling, configureRssPolling,
and configureImapPolling out of utils.server.ts into a dedicated module.
Update imports in deploy.ts and webhooks/route.ts.
2026-04-05 11:39:19 -07:00
Waleed Latif
46a1ea039a refactor(webhooks): move fetchAndProcessAirtablePayloads into airtable handler
Co-locate the ~400-line Airtable payload processing function with its
provider handler. Remove AirtableChange interface from utils.server.ts.
2026-04-05 11:39:19 -07:00
Waleed Latif
1f92950dce refactor(webhooks): move challenge handlers into provider files
Move handleWhatsAppVerification to providers/whatsapp.ts and
handleSlackChallenge to providers/slack.ts. Update processor.ts
imports to point to provider files.
2026-04-05 11:39:19 -07:00
Waleed Latif
7b6b50bbd2 refactor(webhooks): move signature validators into provider handler files
Co-locate each validate*Signature function with its provider handler,
eliminating the circular dependency where handlers imported back from
utils.server.ts. validateJiraSignature is exported from jira.ts for
shared use by confluence.ts.
2026-04-05 11:39:09 -07:00
Waleed Latif
5d9b95a904 fix(webhooks): return 401 when requireAuth is true but no token configured
If a user explicitly sets requireAuth: true, they expect auth to be enforced.
Returning 401 when no token is configured is the correct behavior — this is
an intentional improvement over the original code which silently allowed
unauthenticated access in this case.
2026-04-05 11:39:09 -07:00
Waleed Latif
403e32ff33 fix(webhooks): fix build error from union type indexing in processTriggerFileOutputs
Cast array initializer to Record<string, unknown> to allow string indexing
while preserving array runtime semantics for the return value.
2026-04-05 11:39:09 -07:00
Waleed Latif
ffa586459b fix(webhooks): address PR review feedback
- Restore original fall-through behavior for generic requireAuth with no token
- Replace `any` params with proper types in processor helper functions
- Restore array-aware initializer in processTriggerFileOutputs
2026-04-05 11:39:09 -07:00
Waleed Latif
cc6b80c722 refactor(webhooks): extract provider-specific logic into handler registry 2026-04-05 11:39:09 -07:00
Waleed
a680cec78f fix(core): consolidate ID generation to prevent HTTP self-hosted crashes (#3977)
* fix(core): consolidate ID generation to prevent HTTP self-hosted crashes

crypto.randomUUID() requires a secure context (HTTPS) in browsers,
causing white-screen crashes on self-hosted HTTP deployments. This
replaces all direct usage of crypto.randomUUID(), nanoid, and the uuid
package with a central utility that falls back to crypto.getRandomValues()
which works in all contexts.

- Add generateId(), generateShortId(), isValidUuid() in @/lib/core/utils/uuid
- Replace crypto.randomUUID() imports across ~220 server + client files
- Replace nanoid imports with generateShortId()
- Replace uuid package validate with isValidUuid()
- Remove nanoid dependency from apps/sim and packages/testing
- Remove browser polyfill script from layout.tsx
- Update test mocks to target @/lib/core/utils/uuid
- Update CLAUDE.md, AGENTS.md, cursor rules, claude rules

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

* update bunlock

* fix(core): remove UUID_REGEX shim, use isValidUuid directly

* fix(core): remove deprecated uuid mock helpers that use vi.doMock

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 11:28:54 -07:00
Waleed
235f0748ca feat(files): expand file editor to support more formats, add docx/xlsx preview (#3971)
* feat(files): expand file editor to support more formats, add docx/xlsx preview

* lint

* fix(files): narrow fileData type for closure in docx/xlsx preview effects

* fix(files): address PR review — fix xlsx type, simplify error helper, tighten iframe sandbox

* add mothership read externsions

* fix(files): update upload test — js is now a supported extension

* fix(files): deduplicate code extensions, handle dotless filenames

* fix(files): lower xlsx preview row cap to 1k and type workbookRef properly

Reduces XLSX_MAX_ROWS from 10,000 to 1,000 to prevent browser sluggishness
on large spreadsheets. Types workbookRef with the proper xlsx.WorkBook
interface instead of unknown, removing the unsafe cast.

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

* refactor(files): extract shared DataTable, isolate client-safe constants

- Move SUPPORTED_CODE_EXTENSIONS to validation-constants.ts so client
  components no longer transitively import Node's `path` module
- Extract shared DataTable component used by both CsvPreview and
  XlsxPreview, eliminating duplicated table markup

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

* refactor(validation): remove Node path import, use plain string extraction

Replace `import path from 'path'` with a simple `extractExtension` helper
that does `fileName.slice(fileName.lastIndexOf('.') + 1)`. This removes
the only Node module dependency from validation.ts, making it safe to
import from client components without pulling in a Node polyfill.

Deletes the unnecessary validation-constants.ts that was introduced as
a workaround — the constants now live back in validation.ts where they
belong.

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

* lint

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

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 09:57:49 -07:00
380 changed files with 8314 additions and 7813 deletions

View File

@@ -275,13 +275,15 @@ export const {Service}Block: BlockConfig = {
If the service's API supports programmatic webhook creation, implement automatic webhook registration instead of requiring users to manually configure webhooks. This provides a much better user experience.
All subscription lifecycle logic lives on the provider handler — **no code touches `route.ts` or `provider-subscriptions.ts`**.
### When to Use Automatic Registration
Check the service's API documentation for endpoints like:
- `POST /webhooks` or `POST /hooks` - Create webhook
- `DELETE /webhooks/{id}` - Delete webhook
Services that support this pattern include: Grain, Lemlist, Calendly, Airtable, Webflow, Typeform, etc.
Services that support this pattern include: Grain, Lemlist, Calendly, Airtable, Webflow, Typeform, Ashby, Attio, etc.
### Implementation Steps
@@ -337,188 +339,145 @@ export function {service}SetupInstructions(eventType: string): string {
}
```
#### 3. Add Webhook Creation to API Route
#### 3. Add `createSubscription` and `deleteSubscription` to the Provider Handler
In `apps/sim/app/api/webhooks/route.ts`, add provider-specific logic after the database save:
In `apps/sim/lib/webhooks/providers/{service}.ts`, add both lifecycle methods to your handler. The orchestration layer (`provider-subscriptions.ts`, `deploy.ts`, `route.ts`) calls these automatically — you never touch those files.
```typescript
// --- {Service} specific logic ---
if (savedWebhook && provider === '{service}') {
logger.info(`[${requestId}] {Service} provider detected. Creating webhook subscription.`)
try {
const result = await create{Service}WebhookSubscription(
{
id: savedWebhook.id,
path: savedWebhook.path,
providerConfig: savedWebhook.providerConfig,
},
requestId
)
import { createLogger } from '@sim/logger'
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/providers/subscription-utils'
import type {
DeleteSubscriptionContext,
SubscriptionContext,
SubscriptionResult,
WebhookProviderHandler,
} from '@/lib/webhooks/providers/types'
if (result) {
// Update the webhook record with the external webhook ID
const updatedConfig = {
...(savedWebhook.providerConfig as Record<string, any>),
externalId: result.id,
const logger = createLogger('WebhookProvider:{Service}')
export const {service}Handler: WebhookProviderHandler = {
// ... other methods (verifyAuth, formatInput, etc.) ...
async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> {
try {
const providerConfig = getProviderConfig(ctx.webhook)
const apiKey = providerConfig.apiKey as string | undefined
const triggerId = providerConfig.triggerId as string | undefined
if (!apiKey) {
throw new Error('{Service} API Key is required.')
}
await db
.update(webhook)
.set({
providerConfig: updatedConfig,
updatedAt: new Date(),
})
.where(eq(webhook.id, savedWebhook.id))
savedWebhook.providerConfig = updatedConfig
logger.info(`[${requestId}] Successfully created {Service} webhook`, {
externalHookId: result.id,
webhookId: savedWebhook.id,
// Map trigger IDs to service event types
const eventTypeMap: Record<string, string | undefined> = {
{service}_event_a: 'eventA',
{service}_event_b: 'eventB',
{service}_webhook: undefined, // Generic - no filter
}
const eventType = eventTypeMap[triggerId ?? '']
const notificationUrl = getNotificationUrl(ctx.webhook)
const requestBody: Record<string, unknown> = {
url: notificationUrl,
}
if (eventType) {
requestBody.eventType = eventType
}
const response = await fetch('https://api.{service}.com/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
})
}
} catch (err) {
logger.error(
`[${requestId}] Error creating {Service} webhook subscription, rolling back webhook`,
err
)
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
return NextResponse.json(
{
error: 'Failed to create webhook in {Service}',
details: err instanceof Error ? err.message : 'Unknown error',
},
{ status: 500 }
)
}
}
// --- End {Service} specific logic ---
```
Then add the helper function at the end of the file:
const responseBody = (await response.json()) as Record<string, unknown>
```typescript
async function create{Service}WebhookSubscription(
webhookData: any,
requestId: string
): Promise<{ id: string } | undefined> {
try {
const { path, providerConfig } = webhookData
const { apiKey, triggerId, projectId } = providerConfig || {}
if (!apiKey) {
throw new Error('{Service} API Key is required.')
}
// Map trigger IDs to service event types
const eventTypeMap: Record<string, string | undefined> = {
{service}_event_a: 'eventA',
{service}_event_b: 'eventB',
{service}_webhook: undefined, // Generic - no filter
}
const eventType = eventTypeMap[triggerId]
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
const requestBody: Record<string, any> = {
url: notificationUrl,
}
if (eventType) {
requestBody.eventType = eventType
}
if (projectId) {
requestBody.projectId = projectId
}
const response = await fetch('https://api.{service}.com/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
})
const responseBody = await response.json()
if (!response.ok) {
const errorMessage = responseBody.message || 'Unknown API error'
let userFriendlyMessage = 'Failed to create webhook in {Service}'
if (response.status === 401) {
userFriendlyMessage = 'Invalid API Key. Please verify and try again.'
} else if (errorMessage) {
userFriendlyMessage = `{Service} error: ${errorMessage}`
if (!response.ok) {
const errorMessage = (responseBody.message as string) || 'Unknown API error'
let userFriendlyMessage = 'Failed to create webhook in {Service}'
if (response.status === 401) {
userFriendlyMessage = 'Invalid API Key. Please verify and try again.'
} else if (errorMessage) {
userFriendlyMessage = `{Service} error: ${errorMessage}`
}
throw new Error(userFriendlyMessage)
}
throw new Error(userFriendlyMessage)
}
const externalId = responseBody.id as string | undefined
if (!externalId) {
throw new Error('{Service} webhook created but no ID was returned.')
}
return { id: responseBody.id }
} catch (error: any) {
logger.error(`Exception during {Service} webhook creation`, { error: error.message })
throw error
}
logger.info(`[${ctx.requestId}] Created {Service} webhook ${externalId}`)
return { providerConfigUpdates: { externalId } }
} catch (error: unknown) {
const err = error as Error
logger.error(`[${ctx.requestId}] {Service} webhook creation failed`, {
message: err.message,
})
throw error
}
},
async deleteSubscription(ctx: DeleteSubscriptionContext): Promise<void> {
try {
const config = getProviderConfig(ctx.webhook)
const apiKey = config.apiKey as string | undefined
const externalId = config.externalId as string | undefined
if (!apiKey || !externalId) {
logger.warn(`[${ctx.requestId}] Missing apiKey or externalId, skipping cleanup`)
return
}
const response = await fetch(`https://api.{service}.com/webhooks/${externalId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
})
if (!response.ok && response.status !== 404) {
logger.warn(
`[${ctx.requestId}] Failed to delete {Service} webhook (non-fatal): ${response.status}`
)
} else {
logger.info(`[${ctx.requestId}] Successfully deleted {Service} webhook ${externalId}`)
}
} catch (error) {
logger.warn(`[${ctx.requestId}] Error deleting {Service} webhook (non-fatal)`, error)
}
},
}
```
#### 4. Add Webhook Deletion to Provider Subscriptions
#### How It Works
In `apps/sim/lib/webhooks/provider-subscriptions.ts`:
The orchestration layer handles everything automatically:
1. Add a logger:
```typescript
const {service}Logger = createLogger('{Service}Webhook')
```
1. **Creation**: `provider-subscriptions.ts``createExternalWebhookSubscription()` calls `handler.createSubscription()` → merges `providerConfigUpdates` into the saved webhook record.
2. **Deletion**: `provider-subscriptions.ts``cleanupExternalWebhook()` calls `handler.deleteSubscription()` → errors are caught and logged non-fatally.
3. **Polling config**: `deploy.ts``configurePollingIfNeeded()` calls `handler.configurePolling()` for credential-based providers (Gmail, Outlook, RSS, IMAP).
2. Add the delete function:
```typescript
export async function delete{Service}Webhook(webhook: any, requestId: string): Promise<void> {
try {
const config = getProviderConfig(webhook)
const apiKey = config.apiKey as string | undefined
const externalId = config.externalId as string | undefined
You do NOT need to modify any orchestration files. Just implement the methods on your handler.
if (!apiKey || !externalId) {
{service}Logger.warn(`[${requestId}] Missing apiKey or externalId, skipping cleanup`)
return
}
#### Shared Utilities for Subscriptions
const response = await fetch(`https://api.{service}.com/webhooks/${externalId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
Import from `@/lib/webhooks/providers/subscription-utils`:
if (!response.ok && response.status !== 404) {
{service}Logger.warn(`[${requestId}] Failed to delete webhook (non-fatal): ${response.status}`)
} else {
{service}Logger.info(`[${requestId}] Successfully deleted webhook ${externalId}`)
}
} catch (error) {
{service}Logger.warn(`[${requestId}] Error deleting webhook (non-fatal)`, error)
}
}
```
3. Add to `cleanupExternalWebhook`:
```typescript
export async function cleanupExternalWebhook(...): Promise<void> {
// ... existing providers ...
} else if (webhook.provider === '{service}') {
await delete{Service}Webhook(webhook, requestId)
}
}
```
- `getProviderConfig(webhook)` — safely extract `providerConfig` as `Record<string, unknown>`
- `getNotificationUrl(webhook)` — build the full callback URL: `{baseUrl}/api/webhooks/trigger/{path}`
- `getCredentialOwner(credentialId, requestId)` — resolve OAuth credential to `{ userId, accountId }` (for OAuth-based providers like Airtable, Attio)
### Key Points for Automatic Registration
- **API Key visibility**: Always use `password: true` for API key fields
- **Error handling**: Roll back the database webhook if external creation fails
- **External ID storage**: Save the external webhook ID in `providerConfig.externalId`
- **Graceful cleanup**: Don't fail webhook deletion if cleanup fails (use non-fatal logging)
- **User-friendly errors**: Map HTTP status codes to helpful error messages
- **Error handling**: Throw from `createSubscription` — the orchestration layer catches it, rolls back the DB webhook, and returns a 500
- **External ID storage**: Return `{ providerConfigUpdates: { externalId } }` — the orchestration layer merges it into `providerConfig`
- **Graceful cleanup**: In `deleteSubscription`, catch errors and log non-fatally (never throw)
- **User-friendly errors**: Map HTTP status codes to helpful error messages in `createSubscription`
## The buildTriggerSubBlocks Helper
@@ -552,6 +511,148 @@ All fields automatically have:
- `mode: 'trigger'` - Only shown in trigger mode
- `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected
## Webhook Provider Handler (Optional)
If the service requires **custom webhook auth** (HMAC signatures, token validation), **event matching** (filtering by trigger type), **idempotency dedup**, **custom input formatting**, or **subscription lifecycle** — all of this lives in a single provider handler file.
### Directory
```
apps/sim/lib/webhooks/providers/
├── types.ts # WebhookProviderHandler interface (16 optional methods)
├── utils.ts # Shared helpers (createHmacVerifier, verifyTokenAuth, skipByEventTypes)
├── subscription-utils.ts # Shared subscription helpers (getProviderConfig, getNotificationUrl, getCredentialOwner)
├── registry.ts # Handler map + default handler
├── index.ts # Barrel export
└── {service}.ts # Your provider handler (ALL provider-specific logic here)
```
### When to Create a Handler
| Behavior | Method to implement | Example providers |
|---|---|---|
| HMAC signature auth | `verifyAuth` via `createHmacVerifier` | Ashby, Jira, Linear, Typeform |
| Custom token auth | `verifyAuth` via `verifyTokenAuth` | Generic, Google Forms |
| Event type filtering | `matchEvent` | GitHub, Jira, Confluence, Attio, HubSpot |
| Event skip by type list | `shouldSkipEvent` via `skipByEventTypes` | Stripe, Grain |
| Idempotency dedup | `extractIdempotencyId` | Slack, Stripe, Linear, Jira |
| Custom success response | `formatSuccessResponse` | Slack, Twilio Voice, Microsoft Teams |
| Custom error format | `formatErrorResponse` | Microsoft Teams |
| Custom input formatting | `formatInput` | Slack, Teams, Attio, Ashby, Gmail, Outlook |
| Auto webhook creation | `createSubscription` | Ashby, Grain, Calendly, Airtable, Typeform |
| Auto webhook deletion | `deleteSubscription` | Ashby, Grain, Calendly, Airtable, Typeform |
| Polling setup | `configurePolling` | Gmail, Outlook, RSS, IMAP |
| Challenge/verification | `handleChallenge` | Slack, WhatsApp, Microsoft Teams |
If none of these apply, you do NOT need a handler file. The default handler provides bearer token auth for providers that set `providerConfig.token`.
### Simple Example: HMAC Auth Only
Signature validators are defined as private functions **inside the handler file** (not in a shared utils file):
```typescript
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@/lib/core/security/encryption'
import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
const logger = createLogger('WebhookProvider:{Service}')
function validate{Service}Signature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) return false
if (!signature.startsWith('sha256=')) return false
const provided = signature.substring(7)
const computed = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
return safeCompare(computed, provided)
} catch (error) {
logger.error('Error validating {Service} signature:', error)
return false
}
}
export const {service}Handler: WebhookProviderHandler = {
verifyAuth: createHmacVerifier({
configKey: 'webhookSecret',
headerName: 'X-{Service}-Signature',
validateFn: validate{Service}Signature,
providerLabel: '{Service}',
}),
}
```
### Example: Auth + Event Matching + Idempotency
```typescript
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@/lib/core/security/encryption'
import type { EventMatchContext, WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
const logger = createLogger('WebhookProvider:{Service}')
function validate{Service}Signature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) return false
const computed = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
return safeCompare(computed, signature)
} catch (error) {
logger.error('Error validating {Service} signature:', error)
return false
}
}
export const {service}Handler: WebhookProviderHandler = {
verifyAuth: createHmacVerifier({
configKey: 'webhookSecret',
headerName: 'X-{Service}-Signature',
validateFn: validate{Service}Signature,
providerLabel: '{Service}',
}),
async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) {
const triggerId = providerConfig.triggerId as string | undefined
const obj = body as Record<string, unknown>
if (triggerId && triggerId !== '{service}_webhook') {
const { is{Service}EventMatch } = await import('@/triggers/{service}/utils')
if (!is{Service}EventMatch(triggerId, obj)) {
logger.debug(
`[${requestId}] {Service} event mismatch for trigger ${triggerId}. Skipping.`,
{ webhookId: webhook.id, workflowId: workflow.id, triggerId }
)
return false
}
}
return true
},
extractIdempotencyId(body: unknown) {
const obj = body as Record<string, unknown>
if (obj.id && obj.type) {
return `${obj.type}:${obj.id}`
}
return null
},
}
```
### Registering the Handler
In `apps/sim/lib/webhooks/providers/registry.ts`:
```typescript
import { {service}Handler } from '@/lib/webhooks/providers/{service}'
const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
// ... existing providers (alphabetical) ...
{service}: {service}Handler,
}
```
## Trigger Outputs & Webhook Input Formatting
### Important: Two Sources of Truth
@@ -559,35 +660,48 @@ All fields automatically have:
There are two related but separate concerns:
1. **Trigger `outputs`** - Schema/contract defining what fields SHOULD be available. Used by UI for tag dropdown.
2. **`formatWebhookInput`** - Implementation that transforms raw webhook payload into actual data. Located in `apps/sim/lib/webhooks/utils.server.ts`.
2. **`formatInput` on the handler** - Implementation that transforms raw webhook payload into actual data. Defined in `apps/sim/lib/webhooks/providers/{service}.ts`.
**These MUST be aligned.** The fields returned by `formatWebhookInput` should match what's defined in trigger `outputs`. If they differ:
**These MUST be aligned.** The fields returned by `formatInput` should match what's defined in trigger `outputs`. If they differ:
- Tag dropdown shows fields that don't exist (broken variable resolution)
- Or actual data has fields not shown in dropdown (users can't discover them)
### When to Add a formatWebhookInput Handler
### When to Add `formatInput`
- **Simple providers**: If the raw webhook payload structure already matches your outputs, you don't need a handler. The generic fallback returns `body` directly.
- **Complex providers**: If you need to transform, flatten, extract nested data, compute fields, or handle conditional logic, add a handler.
- **Simple providers**: If the raw webhook payload structure already matches your outputs, you don't need it. The fallback passes through the raw body directly.
- **Complex providers**: If you need to transform, flatten, extract nested data, compute fields, or handle conditional logic, add `formatInput` to your handler.
### Adding a Handler
### Adding `formatInput` to Your Handler
In `apps/sim/lib/webhooks/utils.server.ts`, add a handler block:
In `apps/sim/lib/webhooks/providers/{service}.ts`:
```typescript
if (foundWebhook.provider === '{service}') {
// Transform raw webhook body to match trigger outputs
return {
eventType: body.type,
resourceId: body.data?.id || '',
timestamp: body.created_at,
resource: body.data,
}
import type {
FormatInputContext,
FormatInputResult,
WebhookProviderHandler,
} from '@/lib/webhooks/providers/types'
export const {service}Handler: WebhookProviderHandler = {
// ... other methods ...
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
const b = body as Record<string, unknown>
return {
input: {
eventType: b.type,
resourceId: (b.data as Record<string, unknown>)?.id || '',
timestamp: b.created_at,
resource: b.data,
},
}
},
}
```
**Key rules:**
- Return fields that match your trigger `outputs` definition exactly
- Return `{ input: { ... } }` where the inner object matches your trigger `outputs` definition exactly
- Return `{ input: ..., skip: { message: '...' } }` to skip execution for this event
- No wrapper objects like `webhook: { data: ... }` or `{service}: { ... }`
- No duplication (don't spread body AND add individual fields)
- Use `null` for missing optional data, not empty objects with empty strings
@@ -688,21 +802,25 @@ export const {service}WebhookTrigger: TriggerConfig = {
- [ ] Block has all trigger IDs in `triggers.available`
- [ ] Block spreads all trigger subBlocks: `...getTrigger('id').subBlocks`
### Webhook Provider Handler (`providers/{service}.ts`)
- [ ] Created handler file in `apps/sim/lib/webhooks/providers/{service}.ts`
- [ ] Registered handler in `apps/sim/lib/webhooks/providers/registry.ts` (alphabetical)
- [ ] Signature validator defined as private function inside handler file (not in a shared file)
- [ ] Used `createHmacVerifier` from `providers/utils` for HMAC-based auth
- [ ] Used `verifyTokenAuth` from `providers/utils` for token-based auth
- [ ] Event matching uses dynamic `await import()` for trigger utils
- [ ] Added `formatInput` if webhook payload needs transformation (returns `{ input: ... }`)
### Automatic Webhook Registration (if supported)
- [ ] Added API key field to `build{Service}ExtraFields` with `password: true`
- [ ] Updated setup instructions for automatic webhook creation
- [ ] Added provider-specific logic to `apps/sim/app/api/webhooks/route.ts`
- [ ] Added `create{Service}WebhookSubscription` helper function
- [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts`
- [ ] Added provider to `cleanupExternalWebhook` function
### Webhook Input Formatting
- [ ] Added handler in `apps/sim/lib/webhooks/utils.server.ts` (if custom formatting needed)
- [ ] Handler returns fields matching trigger `outputs` exactly
- [ ] Run `bunx scripts/check-trigger-alignment.ts {service}` to verify alignment
- [ ] Added `createSubscription` method to handler (uses `getNotificationUrl`, `getProviderConfig` from `subscription-utils`)
- [ ] Added `deleteSubscription` method to handler (catches errors, logs non-fatally)
- [ ] NO changes needed to `route.ts`, `provider-subscriptions.ts`, or `deploy.ts`
### Testing
- [ ] Run `bun run type-check` to verify no TypeScript errors
- [ ] Run `bunx scripts/check-trigger-alignment.ts {service}` to verify output alignment
- [ ] Restart dev server to pick up new triggers
- [ ] Test trigger UI shows correctly in the block
- [ ] Test automatic webhook creation works (if applicable)

View File

@@ -9,5 +9,26 @@ Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
## Styling
Never update global styles. Keep all styling local to components.
## ID Generation
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@/lib/core/utils/uuid`:
- `generateId()` — UUID v4, use by default
- `generateShortId(size?)` — short URL-safe ID (default 21 chars), for compact identifiers
Both use `crypto.getRandomValues()` under the hood and work in all contexts including non-secure (HTTP) browsers.
```typescript
// ✗ Bad
import { nanoid } from 'nanoid'
import { v4 as uuidv4 } from 'uuid'
const id = crypto.randomUUID()
// ✓ Good
import { generateId, generateShortId } from '@/lib/core/utils/uuid'
const uuid = generateId()
const shortId = generateShortId()
const tiny = generateShortId(8)
```
## Package Manager
Use `bun` and `bunx`, not `npm` and `npx`.

View File

@@ -16,5 +16,26 @@ Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
## Styling
Never update global styles. Keep all styling local to components.
## ID Generation
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@/lib/core/utils/uuid`:
- `generateId()` — UUID v4, use by default
- `generateShortId(size?)` — short URL-safe ID (default 21 chars), for compact identifiers
Both use `crypto.getRandomValues()` under the hood and work in all contexts including non-secure (HTTP) browsers.
```typescript
// ✗ Bad
import { nanoid } from 'nanoid'
import { v4 as uuidv4 } from 'uuid'
const id = crypto.randomUUID()
// ✓ Good
import { generateId, generateShortId } from '@/lib/core/utils/uuid'
const uuid = generateId()
const shortId = generateShortId()
const tiny = generateShortId(8)
```
## Package Manager
Use `bun` and `bunx`, not `npm` and `npx`.

View File

@@ -7,6 +7,7 @@ You are a professional software engineer. All code must follow best practices: a
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
- **Styling**: Never update global styles. Keep all styling local to components
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid`
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
## Architecture

View File

@@ -7,6 +7,7 @@ You are a professional software engineer. All code must follow best practices: a
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
- **Styling**: Never update global styles. Keep all styling local to components
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid`
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
## Architecture

View File

@@ -1,39 +1,26 @@
'use client'
import { useState } from 'react'
import { ArrowLeft, ChevronLeft } from 'lucide-react'
import Link from 'next/link'
export function BackLink() {
const [isHovered, setIsHovered] = useState(false)
return (
<Link
href='/blog'
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
className='group flex items-center gap-1 text-[var(--landing-text-muted)] text-sm hover:text-[var(--landing-text)]'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<svg
className='h-3 w-3 shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<line
x1='1'
y1='5'
x2='10'
y2='5'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
className='origin-right scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
/>
<path
d='M6.5 2L3.5 5L6.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
className='group-hover/link:-translate-x-[30%] transition-transform duration-200 ease-out'
/>
</svg>
<span className='group-hover:-translate-x-0.5 inline-flex transition-transform duration-200'>
{isHovered ? (
<ArrowLeft className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronLeft className='h-4 w-4' aria-hidden='true' />
)}
</span>
Back to Blog
</Link>
)

View File

@@ -2,51 +2,58 @@ import { Skeleton } from '@/components/emcn'
export default function BlogPostLoading() {
return (
<article className='w-full bg-[var(--landing-bg)]'>
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
<article className='w-full'>
{/* Header area */}
<div className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
{/* Back link */}
<div className='mb-6'>
<Skeleton className='h-[16px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-[60px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
</div>
{/* Image + title row */}
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
{/* Image */}
<div className='w-full flex-shrink-0 md:w-[450px]'>
<Skeleton className='aspect-[450/360] w-full rounded-[5px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='aspect-[450/360] w-full rounded-lg bg-[var(--landing-bg-elevated)]' />
</div>
{/* Title + author */}
<div className='flex flex-1 flex-col justify-between'>
<div>
<Skeleton className='h-[44px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='mt-2 h-[44px] w-[80%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='mt-4 h-[18px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='mt-2 h-[18px] w-[70%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[48px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='mt-2 h-[48px] w-[80%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
</div>
<div className='mt-6 flex items-center gap-6'>
<Skeleton className='h-[12px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<div className='mt-4 flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Skeleton className='h-[20px] w-[20px] rounded-full bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[12px] w-[80px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[24px] w-[24px] rounded-full bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
</div>
<Skeleton className='h-[32px] w-[32px] rounded-[6px] bg-[var(--landing-bg-elevated)]' />
</div>
</div>
</div>
</div>
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
<div className='mx-auto max-w-[900px] px-6 py-16'>
<div className='space-y-4'>
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-[95%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-[88%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='mt-6 h-[24px] w-[200px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-[92%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-[85%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
{/* Divider */}
<Skeleton className='mt-8 h-[1px] w-full bg-[var(--landing-bg-elevated)] sm:mt-12' />
{/* Date + description */}
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
<Skeleton className='h-[16px] w-[120px] flex-shrink-0 rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<div className='flex-1 space-y-2'>
<Skeleton className='h-[20px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[20px] w-[70%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
</div>
</div>
</div>
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
{/* Article body */}
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12'>
<div className='space-y-4'>
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-[95%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-[88%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='mt-6 h-[24px] w-[200px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-[92%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
<Skeleton className='h-[16px] w-[85%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
</div>
</div>
</article>
)
}

View File

@@ -4,7 +4,7 @@ import Link from 'next/link'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
import { FAQ } from '@/lib/blog/faq'
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
import { buildPostGraphJsonLd, buildPostMetadata } from '@/lib/blog/seo'
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { BackLink } from '@/app/(landing)/blog/[slug]/back-link'
import { ShareButton } from '@/app/(landing)/blog/[slug]/share-button'
@@ -30,27 +30,27 @@ export default async function Page({ params }: { params: Promise<{ slug: string
const { slug } = await params
const post = await getPostBySlug(slug)
const Article = post.Content
const graphJsonLd = buildPostGraphJsonLd(post)
const jsonLd = buildArticleJsonLd(post)
const breadcrumbLd = buildBreadcrumbJsonLd(post)
const related = await getRelatedPosts(slug, 3)
return (
<article
className='w-full bg-[var(--landing-bg)]'
itemScope
itemType='https://schema.org/TechArticle'
>
<article className='w-full' itemScope itemType='https://schema.org/BlogPosting'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(graphJsonLd) }}
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<header className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }}
/>
<header className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
<div className='mb-6'>
<BackLink />
</div>
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
<div className='w-full flex-shrink-0 md:w-[450px]'>
<div className='relative w-full overflow-hidden rounded-[5px]'>
<div className='relative w-full overflow-hidden rounded-lg'>
<Image
src={post.ogImage}
alt={post.title}
@@ -65,35 +65,18 @@ export default async function Page({ params }: { params: Promise<{ slug: string
</div>
</div>
<div className='flex flex-1 flex-col justify-between'>
<div>
<h1
className='text-balance font-[430] font-season text-[28px] text-white leading-[110%] tracking-[-0.02em] sm:text-[36px] md:text-[44px] lg:text-[52px]'
itemProp='headline'
>
{post.title}
</h1>
<p className='mt-4 font-[430] font-season text-[var(--landing-text-body)] text-base leading-[150%] tracking-[0.02em] sm:text-lg'>
{post.description}
</p>
</div>
<div className='mt-6 flex items-center gap-6'>
<time
className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'
dateTime={post.date}
itemProp='datePublished'
>
{new Date(post.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</time>
<meta itemProp='dateModified' content={post.updated ?? post.date} />
<h1
className='text-balance font-[500] text-[36px] text-[var(--landing-text)] leading-tight tracking-tight sm:text-[48px] md:text-[56px] lg:text-[64px]'
itemProp='headline'
>
{post.title}
</h1>
<div className='mt-4 flex items-center justify-between'>
<div className='flex items-center gap-3'>
{(post.authors || [post.author]).map((a, idx) => (
<div key={idx} className='flex items-center gap-2'>
{a?.avatarUrl ? (
<Avatar className='size-5'>
<Avatar className='size-6'>
<AvatarImage src={a.avatarUrl} alt={a.name} />
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
</Avatar>
@@ -102,7 +85,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
href={a?.url || '#'}
target='_blank'
rel='noopener noreferrer author'
className='font-martian-mono text-[var(--landing-text-muted)] text-xs uppercase tracking-[0.1em] hover:text-white'
className='text-[var(--landing-text-muted)] text-sm leading-[1.5] hover:text-[var(--landing-text)] sm:text-md'
itemProp='author'
itemScope
itemType='https://schema.org/Person'
@@ -112,72 +95,78 @@ export default async function Page({ params }: { params: Promise<{ slug: string
</div>
))}
</div>
<div className='ml-auto'>
<ShareButton url={`${getBaseUrl()}/blog/${slug}`} title={post.title} />
</div>
<ShareButton url={`${getBaseUrl()}/blog/${slug}`} title={post.title} />
</div>
</div>
</div>
<hr className='mt-8 border-[var(--landing-bg-elevated)] border-t sm:mt-12' />
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
<div className='flex flex-shrink-0 items-center gap-4'>
<time
className='block text-[var(--landing-text-muted)] text-sm leading-[1.5] sm:text-md'
dateTime={post.date}
itemProp='datePublished'
>
{new Date(post.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</time>
<meta itemProp='dateModified' content={post.updated ?? post.date} />
</div>
<div className='flex-1'>
<p className='m-0 block translate-y-[-4px] font-[400] text-[var(--landing-text-muted)] text-lg leading-[1.5] sm:text-[20px] md:text-[26px]'>
{post.description}
</p>
</div>
</div>
</header>
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
<div className='mx-auto max-w-[900px] px-6 py-16' itemProp='articleBody'>
<div className='prose prose-lg prose-invert max-w-none prose-blockquote:border-[var(--landing-border-strong)] prose-hr:border-[var(--landing-bg-elevated)] prose-headings:font-[430] prose-headings:font-season prose-a:text-white prose-blockquote:text-[var(--landing-text-muted)] prose-code:text-white prose-headings:text-white prose-li:text-[var(--landing-text-body)] prose-p:text-[var(--landing-text-body)] prose-strong:text-white prose-headings:tracking-[-0.02em]'>
<Article />
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
</div>
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12' itemProp='articleBody'>
<div className='prose prose-lg prose-invert max-w-none prose-blockquote:border-[var(--landing-border-strong)] prose-hr:border-[var(--landing-bg-elevated)] prose-a:text-[var(--landing-text)] prose-blockquote:text-[var(--landing-text-muted)] prose-code:text-[var(--landing-text)] prose-headings:text-[var(--landing-text)] prose-li:text-[var(--landing-text-muted)] prose-p:text-[var(--landing-text-muted)] prose-strong:text-[var(--landing-text)]'>
<Article />
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
</div>
{related.length > 0 && (
<>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<nav aria-label='Related posts' className='flex'>
{related.map((p) => (
<Link
key={p.slug}
href={`/blog/${p.slug}`}
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] p-6 transition-colors hover:bg-[var(--landing-bg-elevated)] md:border-l md:first:border-l-0'
>
<div className='relative aspect-video w-full overflow-hidden rounded-[5px]'>
<Image
src={p.ogImage}
alt={p.title}
fill
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
className='object-cover'
loading='lazy'
unoptimized
/>
</div>
<div className='flex flex-col gap-2'>
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
</div>
{related.length > 0 && (
<div className='mx-auto max-w-[900px] px-6 pb-24 sm:px-8 md:px-12'>
<h2 className='mb-4 font-[500] text-[24px] text-[var(--landing-text)]'>Related posts</h2>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3'>
{related.map((p) => (
<Link key={p.slug} href={`/blog/${p.slug}`} className='group'>
<div className='overflow-hidden rounded-lg border border-[var(--landing-bg-elevated)]'>
<Image
src={p.ogImage}
alt={p.title}
width={600}
height={315}
className='h-[160px] w-full object-cover'
sizes='(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'
loading='lazy'
unoptimized
/>
<div className='p-3'>
<div className='mb-1 text-[var(--landing-text-muted)] text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
year: '2-digit',
day: 'numeric',
year: 'numeric',
})}
</span>
<h3 className='font-[430] font-season text-lg text-white leading-tight tracking-[-0.01em]'>
</div>
<div className='font-[500] text-[var(--landing-text)] text-sm leading-tight'>
{p.title}
</h3>
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{p.description}
</p>
</div>
</div>
</Link>
))}
</nav>
</>
)}
</div>
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
</div>
</Link>
))}
</div>
</div>
)}
<meta itemProp='publisher' content='Sim' />
<meta itemProp='inLanguage' content='en-US' />
<meta itemProp='keywords' content={post.tags.join(', ')} />
{post.wordCount && <meta itemProp='wordCount' content={String(post.wordCount)} />}
</article>
)
}

View File

@@ -13,29 +13,7 @@ export async function generateMetadata({
const { id } = await params
const posts = (await getAllPostMeta()).filter((p) => p.author.id === id)
const author = posts[0]?.author
const name = author?.name ?? 'Author'
return {
title: `${name} — Sim Blog`,
description: `Read articles by ${name} on the Sim blog.`,
alternates: { canonical: `https://sim.ai/blog/authors/${id}` },
openGraph: {
title: `${name} — Sim Blog`,
description: `Read articles by ${name} on the Sim blog.`,
url: `https://sim.ai/blog/authors/${id}`,
siteName: 'Sim',
type: 'profile',
...(author?.avatarUrl
? { images: [{ url: author.avatarUrl, width: 400, height: 400, alt: name }] }
: {}),
},
twitter: {
card: 'summary',
title: `${name} — Sim Blog`,
description: `Read articles by ${name} on the Sim blog.`,
site: '@simdotai',
...(author?.xHandle ? { creator: `@${author.xHandle}` } : {}),
},
}
return { title: author?.name ?? 'Author' }
}
export default async function AuthorPage({ params }: { params: Promise<{ id: string }> }) {
@@ -49,41 +27,19 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
</main>
)
}
const graphJsonLd = {
const personJsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'Person',
name: author.name,
url: `https://sim.ai/blog/authors/${author.id}`,
sameAs: author.url ? [author.url] : [],
image: author.avatarUrl,
worksFor: {
'@type': 'Organization',
name: 'Sim',
url: 'https://sim.ai',
},
},
{
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
{ '@type': 'ListItem', position: 2, name: 'Blog', item: 'https://sim.ai/blog' },
{
'@type': 'ListItem',
position: 3,
name: author.name,
item: `https://sim.ai/blog/authors/${author.id}`,
},
],
},
],
'@type': 'Person',
name: author.name,
url: `https://sim.ai/blog/authors/${author.id}`,
sameAs: author.url ? [author.url] : [],
image: author.avatarUrl,
}
return (
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(graphJsonLd) }}
dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
/>
<div className='mb-6 flex items-center gap-3'>
{author.avatarUrl ? (

View File

@@ -9,14 +9,8 @@ export default async function StudioLayout({ children }: { children: React.React
'@type': 'Organization',
name: 'Sim',
url: 'https://sim.ai',
description:
'Sim is an open-source platform for building, testing, and deploying AI agent workflows.',
logo: 'https://sim.ai/logo/primary/small.png',
sameAs: [
'https://x.com/simdotai',
'https://github.com/simstudioai/sim',
'https://www.linkedin.com/company/simdotai',
],
sameAs: ['https://x.com/simdotai'],
}
const websiteJsonLd = {
@@ -24,6 +18,11 @@ export default async function StudioLayout({ children }: { children: React.React
'@type': 'WebSite',
name: 'Sim',
url: 'https://sim.ai',
potentialAction: {
'@type': 'SearchAction',
target: 'https://sim.ai/search?q={search_term_string}',
'query-input': 'required name=search_term_string',
},
}
return (

View File

@@ -1,60 +1,11 @@
import type { Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import { Badge } from '@/components/emcn'
import { getAllPostMeta } from '@/lib/blog/registry'
import { buildCollectionPageJsonLd } from '@/lib/blog/seo'
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<{ page?: string; tag?: string }>
}): Promise<Metadata> {
const { page, tag } = await searchParams
const pageNum = Math.max(1, Number(page || 1))
const titleParts = ['Blog']
if (tag) titleParts.push(tag)
if (pageNum > 1) titleParts.push(`Page ${pageNum}`)
const title = titleParts.join(' — ')
const description = tag
? `Sim blog posts tagged "${tag}" — insights and guides for building AI agent workflows.`
: 'Announcements, insights, and guides for building AI agent workflows.'
const canonicalParams = new URLSearchParams()
if (tag) canonicalParams.set('tag', tag)
if (pageNum > 1) canonicalParams.set('page', String(pageNum))
const qs = canonicalParams.toString()
const canonical = `https://sim.ai/blog${qs ? `?${qs}` : ''}`
return {
title,
description,
alternates: { canonical },
openGraph: {
title: `${title} | Sim`,
description,
url: canonical,
siteName: 'Sim',
locale: 'en_US',
type: 'website',
images: [
{
url: 'https://sim.ai/logo/primary/medium.png',
width: 1200,
height: 630,
alt: 'Sim Blog',
},
],
},
twitter: {
card: 'summary_large_image',
title: `${title} | Sim`,
description,
site: '@simdotai',
},
}
export const metadata: Metadata = {
title: 'Blog',
description: 'Announcements, insights, and guides from the Sim team.',
}
export const revalidate = 3600
@@ -86,13 +37,19 @@ export default async function BlogIndex({
const featured = pageNum === 1 ? posts.slice(0, 3) : []
const remaining = pageNum === 1 ? posts.slice(3) : posts
const collectionJsonLd = buildCollectionPageJsonLd()
const blogJsonLd = {
'@context': 'https://schema.org',
'@type': 'Blog',
name: 'Sim Blog',
url: 'https://sim.ai/blog',
description: 'Announcements, insights, and guides for building AI agent workflows.',
}
return (
<section className='bg-[var(--landing-bg)]'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(collectionJsonLd) }}
dangerouslySetInnerHTML={{ __html: JSON.stringify(blogJsonLd) }}
/>
{/* Section header */}
@@ -124,7 +81,7 @@ export default async function BlogIndex({
{/* Featured posts */}
{featured.length > 0 && (
<>
<nav aria-label='Featured posts' className='flex'>
<div className='flex'>
{featured.map((p, index) => (
<Link
key={p.slug}
@@ -132,14 +89,11 @@ export default async function BlogIndex({
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] p-6 transition-colors hover:bg-[var(--landing-bg-elevated)] md:border-l md:first:border-l-0'
>
<div className='relative aspect-video w-full overflow-hidden rounded-[5px]'>
<Image
<img
src={p.ogImage}
alt={p.title}
fill
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
className='object-cover'
priority={index < 3}
unoptimized
className='h-full w-full object-cover'
loading={index < 3 ? 'eager' : 'lazy'}
/>
</div>
<div className='flex flex-col gap-2'>
@@ -158,7 +112,7 @@ export default async function BlogIndex({
</div>
</Link>
))}
</nav>
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
@@ -197,14 +151,12 @@ export default async function BlogIndex({
</div>
{/* Image */}
<div className='relative hidden h-[80px] w-[140px] shrink-0 overflow-hidden rounded-[5px] sm:block'>
<Image
<div className='hidden h-[80px] w-[140px] shrink-0 overflow-hidden rounded-[5px] sm:block'>
<img
src={p.ogImage}
alt={p.title}
fill
sizes='140px'
className='object-cover'
unoptimized
className='h-full w-full object-cover'
loading='lazy'
/>
</div>
</Link>
@@ -214,12 +166,11 @@ export default async function BlogIndex({
{/* Pagination */}
{totalPages > 1 && (
<nav aria-label='Pagination' className='px-6 py-8'>
<div className='px-6 py-8'>
<div className='flex items-center justify-center gap-3'>
{pageNum > 1 && (
<Link
href={`/blog?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
rel='prev'
className='rounded-[5px] border border-[var(--landing-border-strong)] px-3 py-1 text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
Previous
@@ -231,14 +182,13 @@ export default async function BlogIndex({
{pageNum < totalPages && (
<Link
href={`/blog?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
rel='next'
className='rounded-[5px] border border-[var(--landing-border-strong)] px-3 py-1 text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
Next
</Link>
)}
</div>
</nav>
</div>
)}
</div>

View File

@@ -7,18 +7,13 @@ export async function GET() {
const posts = await getAllPostMeta()
const items = posts.slice(0, 50)
const site = 'https://sim.ai'
const lastBuildDate =
items.length > 0 ? new Date(items[0].date).toUTCString() : new Date().toUTCString()
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<rss version="2.0">
<channel>
<title>Sim Blog</title>
<link>${site}</link>
<description>Announcements, insights, and guides for AI agent workflows.</description>
<language>en-us</language>
<lastBuildDate>${lastBuildDate}</lastBuildDate>
<atom:link href="${site}/blog/rss.xml" rel="self" type="application/rss+xml" />
${items
.map(
(p) => `
@@ -31,7 +26,6 @@ export async function GET() {
${(p.authors || [p.author])
.map((a) => `<author><![CDATA[${a.name}${a.url ? ` (${a.url})` : ''}]]></author>`)
.join('\n')}
${p.tags.map((t) => `<category><![CDATA[${t}]]></category>`).join('\n ')}
</item>`
)
.join('')}

View File

@@ -4,42 +4,12 @@ import { getAllTags } from '@/lib/blog/registry'
export const metadata: Metadata = {
title: 'Tags',
description: 'Browse Sim blog posts by topic — AI agents, workflows, integrations, and more.',
alternates: { canonical: 'https://sim.ai/blog/tags' },
openGraph: {
title: 'Blog Tags | Sim',
description: 'Browse Sim blog posts by topic — AI agents, workflows, integrations, and more.',
url: 'https://sim.ai/blog/tags',
siteName: 'Sim',
locale: 'en_US',
type: 'website',
},
twitter: {
card: 'summary',
title: 'Blog Tags | Sim',
description: 'Browse Sim blog posts by topic — AI agents, workflows, integrations, and more.',
site: '@simdotai',
},
}
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
{ '@type': 'ListItem', position: 2, name: 'Blog', item: 'https://sim.ai/blog' },
{ '@type': 'ListItem', position: 3, name: 'Tags', item: 'https://sim.ai/blog/tags' },
],
}
export default async function TagsIndex() {
const tags = await getAllTags()
return (
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
/>
<h1 className='mb-6 font-[500] text-[32px] text-[var(--landing-text)] leading-tight'>
Browse by tag
</h1>

View File

@@ -7,7 +7,7 @@ import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
const logger = createLogger('github-stars')
const INITIAL_STARS = '27.6k'
const INITIAL_STARS = '27k'
/**
* Client component that displays GitHub stars count.

View File

@@ -9,11 +9,11 @@ import { a2aAgent, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, isNull, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
import { sanitizeAgentName } from '@/lib/a2a/utils'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { captureServerEvent } from '@/lib/posthog/server'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
@@ -173,7 +173,7 @@ export async function POST(request: NextRequest) {
skillTags
)
const agentId = uuidv4()
const agentId = generateId()
const agentName = name || sanitizeAgentName(wf.name)
const [agent] = await db

View File

@@ -4,7 +4,6 @@ import { a2aAgent, a2aPushNotificationConfig, a2aTask, workflow } from '@sim/db/
import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { A2A_DEFAULT_TIMEOUT, A2A_MAX_HISTORY_LENGTH } from '@/lib/a2a/constants'
import { notifyTaskStateChange } from '@/lib/a2a/push-notifications'
import {
@@ -18,6 +17,7 @@ import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redi
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { markExecutionCancelled } from '@/lib/execution/cancellation'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
@@ -400,11 +400,11 @@ async function handleMessageSend(
const message = params.message
const taskId = message.taskId || generateTaskId()
const contextId = message.contextId || uuidv4()
const contextId = message.contextId || generateId()
// Distributed lock to prevent concurrent task processing
const lockKey = `a2a:task:${taskId}:lock`
const lockValue = uuidv4()
const lockValue = generateId()
const acquired = await acquireLock(lockKey, lockValue, 60)
if (!acquired) {
@@ -628,12 +628,12 @@ async function handleMessageStream(
}
const message = params.message
const contextId = message.contextId || uuidv4()
const contextId = message.contextId || generateId()
const taskId = message.taskId || generateTaskId()
// Distributed lock to prevent concurrent task processing
const lockKey = `a2a:task:${taskId}:lock`
const lockValue = uuidv4()
const lockValue = generateId()
const acquired = await acquireLock(lockKey, lockValue, 300)
if (!acquired) {
@@ -1427,7 +1427,7 @@ async function handlePushNotificationSet(
.where(eq(a2aPushNotificationConfig.id, existingConfig.id))
} else {
await db.insert(a2aPushNotificationConfig).values({
id: uuidv4(),
id: generateId(),
taskId: params.id,
url: config.url,
token: config.token || null,

View File

@@ -1,7 +1,7 @@
import type { Artifact, Message, PushNotificationConfig, Task, TaskState } from '@a2a-js/sdk'
import { v4 as uuidv4 } from 'uuid'
import { generateInternalToken } from '@/lib/auth/internal'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
/** A2A v0.3 JSON-RPC method names */
export const A2A_METHODS = {
@@ -85,7 +85,7 @@ export function isJSONRPCRequest(obj: unknown): obj is JSONRPCRequest {
}
export function generateTaskId(): string {
return uuidv4()
return generateId()
}
export function createTaskStatus(state: TaskState): { state: TaskState; timestamp: string } {

View File

@@ -2,7 +2,6 @@ import { db } from '@sim/db'
import { academyCertificate, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getCourseById } from '@/lib/academy/content'
@@ -10,6 +9,7 @@ import type { CertificateMetadata } from '@/lib/academy/types'
import { getSession } from '@/lib/auth'
import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { generateShortId } from '@/lib/core/utils/uuid'
const logger = createLogger('AcademyCertificatesAPI')
@@ -106,7 +106,7 @@ export async function POST(req: NextRequest) {
const [certificate] = await db
.insert(academyCertificate)
.values({
id: nanoid(),
id: generateShortId(),
userId: session.user.id,
courseId,
status: 'active',
@@ -211,5 +211,5 @@ export async function GET(req: NextRequest) {
/** Generates a human-readable certificate number, e.g. SIM-2026-A3K9XZ2P */
function generateCertificateNumber(): string {
const year = new Date().getFullYear()
return `SIM-${year}-${nanoid(8).toUpperCase()}`
return `SIM-${year}-${generateShortId(8).toUpperCase()}`
}

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
const logger = createLogger('ShopifyAuthorize')
@@ -161,7 +162,7 @@ export async function GET(request: NextRequest) {
const baseUrl = getBaseUrl()
const redirectUri = `${baseUrl}/api/auth/oauth2/callback/shopify`
const state = crypto.randomUUID()
const state = generateId()
const oauthUrl =
`https://${cleanShop}/admin/oauth/authorize?` +

View File

@@ -1,4 +1,4 @@
import { randomInt, randomUUID } from 'crypto'
import { randomInt } from 'crypto'
import { db } from '@sim/db'
import { chat, verification } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -10,6 +10,7 @@ import { getRedisClient } from '@/lib/core/config/redis'
import { addCorsHeaders, isEmailAllowed } from '@/lib/core/security/deployment'
import { getStorageMethod } from '@/lib/core/storage'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { setChatAuthCookie } from '@/app/api/chat/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -61,7 +62,7 @@ async function storeOTP(email: string, chatId: string, otp: string): Promise<voi
await db.transaction(async (tx) => {
await tx.delete(verification).where(eq(verification.identifier, identifier))
await tx.insert(verification).values({
id: randomUUID(),
id: generateId(),
identifier,
value,
expiresAt,

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { chat, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -7,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { addCorsHeaders, validateAuthToken } from '@/lib/core/security/deployment'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { ChatFiles } from '@/lib/uploads'
@@ -103,7 +103,7 @@ export async function POST(
)
}
const executionId = randomUUID()
const executionId = generateId()
const loggingSession = new LoggingSession(
deployment.workflowId,
executionId,
@@ -150,7 +150,7 @@ export async function POST(
return addCorsHeaders(createErrorResponse('No input provided', 400), request)
}
const executionId = randomUUID()
const executionId = generateId()
const loggingSession = new LoggingSession(deployment.workflowId, executionId, 'chat', requestId)

View File

@@ -27,6 +27,7 @@ import {
createRequestTracker,
createUnauthorizedResponse,
} from '@/lib/copilot/request-helpers'
import { generateId } from '@/lib/core/utils/uuid'
import { captureServerEvent } from '@/lib/posthog/server'
import {
authorizeWorkflowByWorkspacePermission,
@@ -205,7 +206,7 @@ export async function POST(req: NextRequest) {
}
)
const userMessageIdToUse = userMessageId || crypto.randomUUID()
const userMessageIdToUse = userMessageId || generateId()
const reqLogger = logger.withMetadata({
requestId: tracker.requestId,
messageId: userMessageIdToUse,
@@ -406,8 +407,8 @@ export async function POST(req: NextRequest) {
}
if (stream) {
const executionId = crypto.randomUUID()
const runId = crypto.randomUUID()
const executionId = generateId()
const runId = generateId()
const sseStream = createSSEStream({
requestPayload,
userId: authenticatedUserId,
@@ -437,7 +438,7 @@ export async function POST(req: NextRequest) {
if (!result.success) return
const assistantMessage: Record<string, unknown> = {
id: crypto.randomUUID(),
id: generateId(),
role: 'assistant' as const,
content: result.content,
timestamp: new Date().toISOString(),
@@ -515,8 +516,8 @@ export async function POST(req: NextRequest) {
return new Response(sseStream, { headers: SSE_RESPONSE_HEADERS })
}
const nsExecutionId = crypto.randomUUID()
const nsRunId = crypto.randomUUID()
const nsExecutionId = generateId()
const nsRunId = generateId()
if (actualChatId) {
await createRunSegment({
@@ -576,7 +577,7 @@ export async function POST(req: NextRequest) {
}
const assistantMessage = {
id: crypto.randomUUID(),
id: generateId(),
role: 'assistant',
content: responseData.content,
timestamp: new Date().toISOString(),

View File

@@ -3,10 +3,10 @@ import { member, templateCreators } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import type { CreatorProfileDetails } from '@/app/_types/creator-profile'
const logger = createLogger('CreatorProfilesAPI')
@@ -147,7 +147,7 @@ export async function POST(request: NextRequest) {
}
// Create the profile
const profileId = uuidv4()
const profileId = generateId()
const now = new Date()
const details: CreatorProfileDetails = {}

View File

@@ -9,6 +9,7 @@ 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'
import { generateId } from '@/lib/core/utils/uuid'
import { sendEmail } from '@/lib/messaging/email/mailer'
const logger = createLogger('CredentialSetInvite')
@@ -105,12 +106,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const body = await req.json()
const { email } = createInviteSchema.parse(body)
const token = crypto.randomUUID()
const token = generateId()
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 7)
const invitation = {
id: crypto.randomUUID(),
id: generateId(),
credentialSetId: id,
email: email || null,
token,

View File

@@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { generateId } from '@/lib/core/utils/uuid'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
const logger = createLogger('CredentialSetMembers')
@@ -167,7 +168,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
return NextResponse.json({ error: 'Member not found' }, { status: 404 })
}
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
// Use transaction to ensure member deletion + webhook sync are atomic
await db.transaction(async (tx) => {

View File

@@ -10,6 +10,7 @@ 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 { generateId } from '@/lib/core/utils/uuid'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
const logger = createLogger('CredentialSetInviteToken')
@@ -125,11 +126,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
}
const now = new Date()
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
await db.transaction(async (tx) => {
await tx.insert(credentialSetMember).values({
id: crypto.randomUUID(),
id: generateId(),
credentialSetId: invitation.credentialSetId,
userId: session.user.id,
status: 'active',

View File

@@ -5,6 +5,7 @@ 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 { generateId } from '@/lib/core/utils/uuid'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
const logger = createLogger('CredentialSetMemberships')
@@ -60,7 +61,7 @@ export async function DELETE(req: NextRequest) {
}
try {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
// Use transaction to ensure revocation + webhook sync are atomic
await db.transaction(async (tx) => {

View File

@@ -7,6 +7,7 @@ import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { generateId } from '@/lib/core/utils/uuid'
const logger = createLogger('CredentialSets')
@@ -150,7 +151,7 @@ export async function POST(req: Request) {
const now = new Date()
const newCredentialSet = {
id: crypto.randomUUID(),
id: generateId(),
organizationId,
name,
description: description || null,

View File

@@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('CredentialMembersAPI')
@@ -133,7 +134,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
}
await db.insert(credentialMember).values({
id: crypto.randomUUID(),
id: generateId(),
credentialId,
userId,
role,

View File

@@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { generateId } from '@/lib/core/utils/uuid'
import { getCredentialActorContext } from '@/lib/credentials/access'
import {
syncPersonalEnvCredentialsForUser,
@@ -273,7 +274,7 @@ export async function DELETE(
await db
.insert(workspaceEnvironment)
.values({
id: workspaceRow?.id || crypto.randomUUID(),
id: workspaceRow?.id || generateId(),
workspaceId: access.credential.workspaceId,
variables: current,
createdAt: workspaceRow?.createdAt || new Date(),

View File

@@ -5,6 +5,7 @@ import { and, eq, lt } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('CredentialDraftAPI')
@@ -75,7 +76,7 @@ export async function POST(request: Request) {
await db
.insert(pendingCredentialDraft)
.values({
id: crypto.randomUUID(),
id: generateId(),
userId,
workspaceId,
providerId,

View File

@@ -7,6 +7,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { getWorkspaceMemberUserIds } from '@/lib/credentials/environment'
import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth'
import { getServiceConfigByProviderId } from '@/lib/oauth'
@@ -536,7 +537,7 @@ export async function POST(request: NextRequest) {
}
const now = new Date()
const credentialId = crypto.randomUUID()
const credentialId = generateId()
const [workspaceRow] = await db
.select({ ownerId: workspace.ownerId })
.from(workspace)
@@ -565,7 +566,7 @@ export async function POST(request: NextRequest) {
if (workspaceUserIds.length > 0) {
for (const memberUserId of workspaceUserIds) {
await tx.insert(credentialMember).values({
id: crypto.randomUUID(),
id: generateId(),
credentialId,
userId: memberUserId,
role:
@@ -582,7 +583,7 @@ export async function POST(request: NextRequest) {
}
} else {
await tx.insert(credentialMember).values({
id: crypto.randomUUID(),
id: generateId(),
credentialId,
userId: session.user.id,
role: 'admin',

View File

@@ -8,6 +8,7 @@ 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 { generateId } from '@/lib/core/utils/uuid'
import { syncPersonalEnvCredentialsForUser } from '@/lib/credentials/environment'
import type { EnvironmentVariable } from '@/stores/settings/environment'
@@ -42,7 +43,7 @@ export async function POST(req: NextRequest) {
await db
.insert(environment)
.values({
id: crypto.randomUUID(),
id: generateId(),
userId: session.user.id,
variables: encryptedVariables,
updatedAt: new Date(),

View File

@@ -82,8 +82,12 @@ vi.mock('drizzle-orm', () => ({
sql: vi.fn((strings: unknown, ...values: unknown[]) => ({ type: 'sql', sql: strings, values })),
}))
vi.mock('uuid', () => ({
v4: vi.fn().mockReturnValue('test-uuid'),
vi.mock('@/lib/core/utils/uuid', () => ({
generateId: vi.fn(() => 'test-uuid'),
generateShortId: vi.fn(() => 'mock-short-id'),
isValidUuid: vi.fn((v: string) =>
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v)
),
}))
vi.mock('@/lib/auth', () => ({

View File

@@ -91,8 +91,12 @@ vi.mock('drizzle-orm', () => ({
sql: vi.fn((strings: unknown, ...values: unknown[]) => ({ type: 'sql', sql: strings, values })),
}))
vi.mock('uuid', () => ({
v4: vi.fn().mockReturnValue('test-uuid'),
vi.mock('@/lib/core/utils/uuid', () => ({
generateId: vi.fn(() => 'test-uuid'),
generateShortId: vi.fn(() => 'mock-short-id'),
isValidUuid: vi.fn((v: string) =>
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v)
),
}))
vi.mock('@/lib/auth', () => ({
@@ -458,10 +462,10 @@ describe('File Upload Security Tests', () => {
expect(response.status).toBe(200)
})
it('should reject JavaScript files', async () => {
it('should reject unsupported file types', async () => {
const formData = new FormData()
const maliciousJs = 'alert("XSS")'
const file = new File([maliciousJs], 'malicious.js', { type: 'application/javascript' })
const content = 'binary data'
const file = new File([content], 'archive.exe', { type: 'application/octet-stream' })
formData.append('file', file)
formData.append('context', 'workspace')
formData.append('workspaceId', 'test-workspace-id')
@@ -475,7 +479,7 @@ describe('File Upload Security Tests', () => {
expect(response.status).toBe(400)
const data = await response.json()
expect(data.message).toContain("File type 'js' is not allowed")
expect(data.message).toContain("File type 'exe' is not allowed")
})
it('should reject files without extensions', async () => {

View File

@@ -8,6 +8,7 @@ import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/works
import { isImageFileType } from '@/lib/uploads/utils/file-utils'
import {
SUPPORTED_AUDIO_EXTENSIONS,
SUPPORTED_CODE_EXTENSIONS,
SUPPORTED_DOCUMENT_EXTENSIONS,
SUPPORTED_VIDEO_EXTENSIONS,
validateFileType,
@@ -23,6 +24,7 @@ const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'] as const
const ALLOWED_EXTENSIONS = new Set<string>([
...SUPPORTED_DOCUMENT_EXTENSIONS,
...SUPPORTED_CODE_EXTENSIONS,
...IMAGE_EXTENSIONS,
...SUPPORTED_AUDIO_EXTENSIONS,
...SUPPORTED_VIDEO_EXTENSIONS,

View File

@@ -7,6 +7,7 @@ import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
@@ -67,7 +68,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const targetWorkspaceId = workspaceId || sourceFolder.workspaceId
const { newFolderId, folderMapping } = await db.transaction(async (tx) => {
const newFolderId = clientNewId || crypto.randomUUID()
const newFolderId = clientNewId || generateId()
const now = new Date()
const targetParentId = parentId ?? sourceFolder.parentId
@@ -227,7 +228,7 @@ async function duplicateFolderStructure(
)
for (const childFolder of childFolders) {
const newChildFolderId = crypto.randomUUID()
const newChildFolderId = generateId()
folderMapping.set(childFolder.id, newChildFolderId)
await tx.insert(workflowFolder).values({

View File

@@ -6,6 +6,7 @@ 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 { generateId } from '@/lib/core/utils/uuid'
import { captureServerEvent } from '@/lib/posthog/server'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
@@ -92,7 +93,7 @@ export async function POST(request: NextRequest) {
)
}
const id = clientId || crypto.randomUUID()
const id = clientId || generateId()
const newFolder = await db.transaction(async (tx) => {
let sortOrder: number

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { form, workflow, workflowBlocks } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -7,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { addCorsHeaders, validateAuthToken } from '@/lib/core/security/deployment'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
@@ -119,7 +119,7 @@ export async function POST(
)
}
const executionId = randomUUID()
const executionId = generateId()
const loggingSession = new LoggingSession(
deployment.workflowId,
executionId,
@@ -165,7 +165,7 @@ export async function POST(
return addCorsHeaders(createErrorResponse('No form data provided', 400), request)
}
const executionId = randomUUID()
const executionId = generateId()
const loggingSession = new LoggingSession(deployment.workflowId, executionId, 'form', requestId)
const preprocessResult = await preprocessExecution({

View File

@@ -3,13 +3,13 @@ import { form } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { isDev } from '@/lib/core/config/feature-flags'
import { encryptSecret } from '@/lib/core/security/encryption'
import { getEmailDomain } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { deployWorkflow } from '@/lib/workflows/persistence/utils'
import {
checkWorkflowAccessForFormCreation,
@@ -158,7 +158,7 @@ export async function POST(request: NextRequest) {
encryptedPassword = encrypted
}
const id = uuidv4()
const id = generateId()
logger.info('Creating form deployment with values:', {
workflowId,

View File

@@ -9,6 +9,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { hasLiveSyncAccess } from '@/lib/billing/core/subscription'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
import { allocateTagSlots } from '@/lib/knowledge/constants'
import { createTagDefinition } from '@/lib/knowledge/tags/service'
@@ -211,7 +212,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
}
const now = new Date()
const connectorId = crypto.randomUUID()
const connectorId = generateId()
const nextSyncAt =
syncIntervalMinutes > 0 ? new Date(now.getTime() + syncIntervalMinutes * 60 * 1000) : null

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { deleteChunk, updateChunk } from '@/lib/knowledge/chunks/service'
import { checkChunkAccess } from '@/app/api/knowledge/utils'
@@ -17,7 +17,7 @@ export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string; chunkId: string }> }
) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId, documentId, chunkId } = await params
try {
@@ -65,7 +65,7 @@ export async function PUT(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string; chunkId: string }> }
) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId, documentId, chunkId } = await params
try {
@@ -147,7 +147,7 @@ export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string; chunkId: string }> }
) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId, documentId, chunkId } = await params
try {

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
import {
cleanupUnusedTagDefinitions,
@@ -34,7 +34,7 @@ export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string }> }
) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId, documentId } = await params
try {
@@ -79,7 +79,7 @@ export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string }> }
) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId, documentId } = await params
try {
@@ -160,7 +160,7 @@ export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string }> }
) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId, documentId } = await params
const { searchParams } = new URL(req.url)
const action = searchParams.get('action') // 'cleanup' or 'all'

View File

@@ -1,10 +1,10 @@
import { randomUUID } from 'crypto'
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 { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
bulkDocumentOperation,
bulkDocumentOperationByFilter,
@@ -66,7 +66,7 @@ const BulkUpdateDocumentsSchema = z
})
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId } = await params
try {
@@ -164,7 +164,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
}
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId } = await params
try {
@@ -398,7 +398,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
}
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId } = await params
try {

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { document } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -7,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
createDocumentRecords,
deleteDocument,
@@ -35,7 +35,7 @@ const UpsertDocumentSchema = z.object({
})
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId } = await params
try {

View File

@@ -1,7 +1,7 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { getNextAvailableSlot, getTagDefinitions } from '@/lib/knowledge/tags/service'
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
@@ -9,7 +9,7 @@ const logger = createLogger('NextAvailableSlotAPI')
// GET /api/knowledge/[id]/next-available-slot - Get the next available tag slot for a knowledge base and field type
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId } = await params
const { searchParams } = new URL(req.url)
const fieldType = searchParams.get('fieldType')

View File

@@ -1,7 +1,7 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { deleteTagDefinition } from '@/lib/knowledge/tags/service'
import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
@@ -14,7 +14,7 @@ export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string; tagId: string }> }
) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId, tagId } = await params
try {

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
@@ -13,7 +13,7 @@ const logger = createLogger('KnowledgeBaseTagDefinitionsAPI')
// GET /api/knowledge/[id]/tag-definitions - Get all tag definitions for a knowledge base
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId } = await params
try {
@@ -53,7 +53,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
// POST /api/knowledge/[id]/tag-definitions - Create a new tag definition
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId } = await params
try {

View File

@@ -1,7 +1,7 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { getTagUsage } from '@/lib/knowledge/tags/service'
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
@@ -11,7 +11,7 @@ const logger = createLogger('TagUsageAPI')
// GET /api/knowledge/[id]/tag-usage - Get usage statistics for all tag definitions
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const { id: knowledgeBaseId } = await params
try {

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'node:crypto'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import {
@@ -30,6 +29,7 @@ import { DIRECT_TOOL_DEFS, SUBAGENT_TOOL_DEFS } from '@/lib/copilot/tools/mcp/de
import { env } from '@/lib/core/config/env'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import {
authorizeWorkflowByWorkspacePermission,
resolveWorkflowIdForUser,
@@ -638,7 +638,7 @@ async function handleDirectToolCall(
)
const toolCall = {
id: randomUUID(),
id: generateId(),
name: toolDef.toolId,
status: 'pending' as const,
params: args as Record<string, any>,
@@ -715,7 +715,7 @@ async function handleBuildToolCall(
}
}
const chatId = randomUUID()
const chatId = generateId()
const requestPayload = {
message: requestText,
@@ -724,12 +724,12 @@ async function handleBuildToolCall(
model: DEFAULT_COPILOT_MODEL,
mode: 'agent',
commands: ['fast'],
messageId: randomUUID(),
messageId: generateId(),
chatId,
}
const executionId = crypto.randomUUID()
const runId = crypto.randomUUID()
const executionId = generateId()
const runId = generateId()
const messageId = requestPayload.messageId as string
await createRunSegment({

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { generateId } from '@/lib/core/utils/uuid'
import {
McpDnsResolutionError,
McpDomainNotAllowedError,
@@ -102,7 +103,7 @@ export const POST = withMcpAuth('write')(
throw e
}
const serverId = body.url ? generateMcpServerId(workspaceId, body.url) : crypto.randomUUID()
const serverId = body.url ? generateMcpServerId(workspaceId, body.url) : generateId()
const [existingServer] = await db
.select({ id: mcpServers.id, deletedAt: mcpServers.deletedAt })

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { generateId } from '@/lib/core/utils/uuid'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpPubSub } from '@/lib/mcp/pubsub'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
@@ -193,7 +194,7 @@ export const POST = withMcpAuth<RouteParams>('write')(
? body.parameterSchema
: await generateParameterSchemaForWorkflow(body.workflowId)
const toolId = crypto.randomUUID()
const toolId = generateId()
const [tool] = await db
.insert(workflowMcpTool)
.values({

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, inArray, isNull, sql } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { generateId } from '@/lib/core/utils/uuid'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpPubSub } from '@/lib/mcp/pubsub'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
@@ -112,7 +113,7 @@ export const POST = withMcpAuth('write')(
)
}
const serverId = crypto.randomUUID()
const serverId = generateId()
const [server] = await db
.insert(workflowMcpServer)
@@ -168,7 +169,7 @@ export const POST = withMcpAuth('write')(
const parameterSchema = await generateParameterSchemaForWorkflow(workflowRecord.id)
const toolId = crypto.randomUUID()
const toolId = generateId()
await db.insert(workflowMcpTool).values({
id: toolId,
serverId,

View File

@@ -5,6 +5,7 @@ import { and, eq, isNull, like } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('MemoryAPI')
@@ -163,7 +164,7 @@ export async function POST(request: NextRequest) {
const initialData = Array.isArray(data) ? data : [data]
const now = new Date()
const id = `mem_${crypto.randomUUID().replace(/-/g, '')}`
const id = `mem_${generateId().replace(/-/g, '')}`
const { sql } = await import('drizzle-orm')

View File

@@ -17,6 +17,7 @@ import { processContextsServer, resolveActiveResourceContext } from '@/lib/copil
import { createRequestTracker, createUnauthorizedResponse } from '@/lib/copilot/request-helpers'
import { taskPubSub } from '@/lib/copilot/task-events'
import { generateWorkspaceContext } from '@/lib/copilot/workspace-context'
import { generateId } from '@/lib/core/utils/uuid'
import {
assertActiveWorkspaceAccess,
getUserEntityPermissions,
@@ -109,7 +110,7 @@ export async function POST(req: NextRequest) {
userTimezone,
} = MothershipMessageSchema.parse(body)
const userMessageId = providedMessageId || crypto.randomUUID()
const userMessageId = providedMessageId || generateId()
userMessageIdForLogs = userMessageId
const reqLogger = logger.withMetadata({
requestId: tracker.requestId,
@@ -280,8 +281,8 @@ export async function POST(req: NextRequest) {
}
}
const executionId = crypto.randomUUID()
const runId = crypto.randomUUID()
const executionId = generateId()
const runId = generateId()
const stream = createSSEStream({
requestPayload,
userId: authenticatedUserId,
@@ -310,7 +311,7 @@ export async function POST(req: NextRequest) {
if (!result.success) return
const assistantMessage: Record<string, unknown> = {
id: crypto.randomUUID(),
id: generateId(),
role: 'assistant' as const,
content: result.content,
timestamp: new Date().toISOString(),

View File

@@ -7,6 +7,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { releasePendingChatStream } from '@/lib/copilot/chat-streaming'
import { taskPubSub } from '@/lib/copilot/task-events'
import { generateId } from '@/lib/core/utils/uuid'
const logger = createLogger('MothershipChatStopAPI')
@@ -71,7 +72,7 @@ export async function POST(req: NextRequest) {
if (hasContent || hasBlocks) {
const assistantMessage: Record<string, unknown> = {
id: crypto.randomUUID(),
id: generateId(),
role: 'assistant' as const,
content,
timestamp: new Date().toISOString(),

View File

@@ -6,6 +6,7 @@ import { createRunSegment } from '@/lib/copilot/async-runs/repository'
import { buildIntegrationToolSchemas } from '@/lib/copilot/chat-payload'
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
import { generateWorkspaceContext } from '@/lib/copilot/workspace-context'
import { generateId } from '@/lib/core/utils/uuid'
import {
assertActiveWorkspaceAccess,
getUserEntityPermissions,
@@ -50,8 +51,8 @@ export async function POST(req: NextRequest) {
await assertActiveWorkspaceAccess(workspaceId, userId)
const effectiveChatId = chatId || crypto.randomUUID()
messageId = crypto.randomUUID()
const effectiveChatId = chatId || generateId()
messageId = generateId()
const reqLogger = logger.withMetadata({ messageId })
const [workspaceContext, integrationTools, userPermission] = await Promise.all([
generateWorkspaceContext(workspaceId, userId),
@@ -72,8 +73,8 @@ export async function POST(req: NextRequest) {
...(userPermission ? { userPermission } : {}),
}
const executionId = crypto.randomUUID()
const runId = crypto.randomUUID()
const executionId = generateId()
const runId = generateId()
await createRunSegment({
id: runId,

View File

@@ -1,8 +1,8 @@
import { createLogger } from '@sim/logger'
import { nanoid } from 'nanoid'
import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { acquireLock, releaseLock } from '@/lib/core/config/redis'
import { generateShortId } from '@/lib/core/utils/uuid'
import { pollInactivityAlerts } from '@/lib/notifications/inactivity-polling'
const logger = createLogger('InactivityAlertPoll')
@@ -13,7 +13,7 @@ const LOCK_KEY = 'inactivity-alert-polling-lock'
const LOCK_TTL_SECONDS = 120
export async function GET(request: NextRequest) {
const requestId = nanoid()
const requestId = generateShortId()
logger.info(`Inactivity alert polling triggered (${requestId})`)
let lockAcquired = false

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import {
invitation,
@@ -27,6 +26,7 @@ import { isOrgPlan, sqlIsPro } from '@/lib/billing/plan-helpers'
import { requireStripeClient } from '@/lib/billing/stripe-client'
import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment'
import { sendEmail } from '@/lib/messaging/email/mailer'
@@ -321,7 +321,7 @@ export async function PUT(
if (status === 'accepted') {
await tx.insert(member).values({
id: randomUUID(),
id: generateId(),
userId: session.user.id,
organizationId,
role: orgInvitation.role,
@@ -423,7 +423,7 @@ export async function PUT(
if (autoAddGroup) {
await tx.insert(permissionGroupMember).values({
id: randomUUID(),
id: generateId(),
permissionGroupId: autoAddGroup.id,
userId: session.user.id,
assignedBy: null,
@@ -497,7 +497,7 @@ export async function PUT(
}
} else {
await tx.insert(permissions).values({
id: randomUUID(),
id: generateId(),
entityType: 'workspace',
entityId: wsInvitation.workspaceId,
userId: session.user.id,

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import {
invitation,
@@ -24,6 +23,7 @@ import {
validateSeatAvailability,
} from '@/lib/billing/validation/seat-management'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
@@ -293,7 +293,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
const invitationsToCreate = emailsToInvite.map((email: string) => ({
id: randomUUID(),
id: generateId(),
email,
inviterId: session.user.id,
organizationId,
@@ -310,8 +310,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
for (const email of emailsToInvite) {
const orgInviteForEmail = invitationsToCreate.find((inv) => inv.email === email)
for (const wsInvitation of validWorkspaceInvitations) {
const wsInvitationId = randomUUID()
const token = randomUUID()
const wsInvitationId = generateId()
const token = generateId()
await db.insert(workspaceInvitation).values({
id: wsInvitationId,

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { invitation, member, organization, user, userStats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -10,6 +9,7 @@ import { getSession } from '@/lib/auth'
import { getUserUsageData } from '@/lib/billing/core/usage'
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
@@ -231,7 +231,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
}
// Create invitation
const invitationId = randomUUID()
const invitationId = generateId()
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 7) // 7 days expiry

View File

@@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { hasAccessControlAccess } from '@/lib/billing'
import { generateId } from '@/lib/core/utils/uuid'
const logger = createLogger('PermissionGroupBulkMembers')
@@ -129,7 +130,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
}
const newMembers = usersToAdd.map((userId) => ({
id: crypto.randomUUID(),
id: generateId(),
permissionGroupId: id,
userId,
assignedBy: session.user.id,

View File

@@ -7,6 +7,7 @@ import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasAccessControlAccess } from '@/lib/billing'
import { generateId } from '@/lib/core/utils/uuid'
const logger = createLogger('PermissionGroupMembers')
@@ -137,7 +138,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
}
const memberData = {
id: crypto.randomUUID(),
id: generateId(),
permissionGroupId: id,
userId,
assignedBy: session.user.id,

View File

@@ -7,6 +7,7 @@ import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasAccessControlAccess } from '@/lib/billing'
import { generateId } from '@/lib/core/utils/uuid'
import {
DEFAULT_PERMISSION_GROUP_CONFIG,
type PermissionGroupConfig,
@@ -181,7 +182,7 @@ export async function POST(req: Request) {
const now = new Date()
const newGroup = {
id: crypto.randomUUID(),
id: generateId(),
organizationId,
name,
description: description || null,

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { AuthType } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
@@ -60,7 +60,7 @@ export async function POST(
userId = billedAccountUserId
}
const resumeExecutionId = randomUUID()
const resumeExecutionId = generateId()
const requestId = generateRequestId()
logger.info(`[${requestId}] Preprocessing resume execution`, {

View File

@@ -132,8 +132,12 @@ vi.mock('@sim/db', () => ({
},
}))
vi.mock('uuid', () => ({
v4: vi.fn().mockReturnValue('schedule-execution-1'),
vi.mock('@/lib/core/utils/uuid', () => ({
generateId: vi.fn(() => 'schedule-execution-1'),
generateShortId: vi.fn(() => 'mock-short-id'),
isValidUuid: vi.fn((v: string) =>
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v)
),
}))
import { GET } from './route'

View File

@@ -2,11 +2,11 @@ import { db, workflowDeploymentVersion, workflowSchedule } from '@sim/db'
import { createLogger } from '@sim/logger'
import { and, eq, isNull, lt, lte, ne, not, or, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { verifyCronAuth } from '@/lib/auth/internal'
import { getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs'
import { createBullMQJobData, isBullMQEnabled } from '@/lib/core/bullmq'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { enqueueWorkspaceDispatch } from '@/lib/core/workspace-dispatch'
import {
executeJobInline,
@@ -89,7 +89,7 @@ export async function GET(request: NextRequest) {
const schedulePromises = dueSchedules.map(async (schedule) => {
const queueTime = schedule.lastQueuedAt ?? queuedAt
const executionId = uuidv4()
const executionId = generateId()
const correlation = {
executionId,
requestId,

View File

@@ -5,6 +5,7 @@ import { and, eq, isNull, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { captureServerEvent } from '@/lib/posthog/server'
import { validateCronExpression } from '@/lib/workflows/schedules/utils'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
@@ -249,7 +250,7 @@ export async function POST(req: NextRequest) {
}
const now = new Date()
const id = crypto.randomUUID()
const id = generateId()
await db.insert(workflowSchedule).values({
id,

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
import {
@@ -118,7 +119,7 @@ export async function POST(request: NextRequest) {
}
// Create new workflow record
const newWorkflowId = crypto.randomUUID()
const newWorkflowId = generateId()
const now = new Date()
const dedupedName = await deduplicateWorkflowName(
`[Debug Import] ${sourceWorkflow.name}`,

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import {
batchInsertRows,
createTable,
@@ -225,7 +226,7 @@ export async function POST(request: NextRequest) {
let inserted = 0
for (let i = 0; i < coerced.length; i += MAX_BATCH_SIZE) {
const batch = coerced.slice(i, i + MAX_BATCH_SIZE)
const batchRequestId = crypto.randomUUID().slice(0, 8)
const batchRequestId = generateId().slice(0, 8)
const result = await batchInsertRows(
{ tableId: table.id, rows: batch, workspaceId, userId: authResult.userId },
table,

View File

@@ -3,9 +3,9 @@ import { templateStars, templates } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
const logger = createLogger('TemplateStarAPI')
@@ -86,7 +86,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
await db.transaction(async (tx) => {
// Add the star record
await tx.insert(templateStars).values({
id: uuidv4(),
id: generateId(),
userId: session.user.id,
templateId: id,
starredAt: new Date(),

View File

@@ -3,10 +3,10 @@ import { templates, workflow, workflowDeploymentVersion } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { canAccessTemplate, verifyTemplateOwnership } from '@/lib/templates/permissions'
import {
type RegenerateStateInput,
@@ -93,7 +93,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const templateData = template[0]
// Create a new workflow ID
const newWorkflowId = uuidv4()
const newWorkflowId = generateId()
const now = new Date()
// Extract variables from the template state and remap to the new workflow
@@ -104,7 +104,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
if (!templateVariables || typeof templateVariables !== 'object') return {}
const mapped: Record<string, any> = {}
for (const [, variable] of Object.entries(templateVariables)) {
const newVarId = uuidv4()
const newVarId = generateId()
mapped[newVarId] = { ...variable, id: newVarId, workflowId: newWorkflowId }
}
return mapped
@@ -178,7 +178,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
// Create a deployment version for the new workflow
if (templateData.state) {
const newDeploymentVersionId = uuidv4()
const newDeploymentVersionId = generateId()
await tx.insert(workflowDeploymentVersion).values({
id: newDeploymentVersionId,
workflowId: newWorkflowId,

View File

@@ -9,11 +9,11 @@ import {
import { createLogger } from '@sim/logger'
import { and, desc, eq, ilike, or, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { canAccessTemplate, verifyEffectiveSuperUser } from '@/lib/templates/permissions'
import {
extractRequiredCredentials,
@@ -267,7 +267,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: permissionError || 'Access denied' }, { status: 403 })
}
const templateId = uuidv4()
const templateId = generateId()
const now = new Date()
// Get the active deployment version for the workflow to copy its state

View File

@@ -6,6 +6,7 @@ import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
export const dynamic = 'force-dynamic'
@@ -142,7 +143,7 @@ export async function POST(request: NextRequest) {
const message: Message = {
kind: 'message',
messageId: crypto.randomUUID(),
messageId: generateId(),
role: 'user',
parts,
...(validatedData.taskId && { taskId: validatedData.taskId }),

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createRawDynamoDBClient, describeTable, listTables } from '@/app/api/tools/dynamodb/utils'
const logger = createLogger('DynamoDBIntrospectAPI')
@@ -15,7 +15,7 @@ const IntrospectSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
const logger = createLogger('MongoDBDeleteAPI')
@@ -38,7 +38,7 @@ const DeleteSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let client = null
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createMongoDBConnection, sanitizeCollectionName, validatePipeline } from '../utils'
const logger = createLogger('MongoDBExecuteAPI')
@@ -30,7 +30,7 @@ const ExecuteSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let client = null
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createMongoDBConnection, sanitizeCollectionName } from '../utils'
const logger = createLogger('MongoDBInsertAPI')
@@ -35,7 +35,7 @@ const InsertSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let client = null
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createMongoDBConnection, executeIntrospect } from '../utils'
const logger = createLogger('MongoDBIntrospectAPI')
@@ -18,7 +18,7 @@ const IntrospectSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let client = null
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
const logger = createLogger('MongoDBQueryAPI')
@@ -47,7 +47,7 @@ const QuerySchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let client = null
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createMongoDBConnection, sanitizeCollectionName, validateFilter } from '../utils'
const logger = createLogger('MongoDBUpdateAPI')
@@ -57,7 +57,7 @@ const UpdateSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let client = null
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { buildDeleteQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLDeleteAPI')
@@ -19,7 +19,7 @@ const DeleteSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLExecuteAPI')
@@ -18,7 +18,7 @@ const ExecuteSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { buildInsertQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLInsertAPI')
@@ -40,7 +40,7 @@ const InsertSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createMySQLConnection, executeIntrospect } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLIntrospectAPI')
@@ -17,7 +17,7 @@ const IntrospectSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLQueryAPI')
@@ -18,7 +18,7 @@ const QuerySchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { buildUpdateQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
const logger = createLogger('MySQLUpdateAPI')
@@ -38,7 +38,7 @@ const UpdateSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
convertNeo4jTypesToJSON,
createNeo4jDriver,
@@ -23,7 +23,7 @@ const CreateSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let driver = null
let session = null

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createNeo4jDriver, validateCypherQuery } from '@/app/api/tools/neo4j/utils'
const logger = createLogger('Neo4jDeleteAPI')
@@ -20,7 +20,7 @@ const DeleteSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let driver = null
let session = null

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
convertNeo4jTypesToJSON,
createNeo4jDriver,
@@ -23,7 +23,7 @@ const ExecuteSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let driver = null
let session = null

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { createNeo4jDriver } from '@/app/api/tools/neo4j/utils'
import type { Neo4jNodeSchema, Neo4jRelationshipSchema } from '@/tools/neo4j/types'
@@ -18,7 +18,7 @@ const IntrospectSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let driver = null
let session = null

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
convertNeo4jTypesToJSON,
createNeo4jDriver,
@@ -23,7 +23,7 @@ const MergeSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let driver = null
let session = null

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
convertNeo4jTypesToJSON,
createNeo4jDriver,
@@ -23,7 +23,7 @@ const QuerySchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let driver = null
let session = null

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
convertNeo4jTypesToJSON,
createNeo4jDriver,
@@ -23,7 +23,7 @@ const UpdateSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
let driver = null
let session = null

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -6,6 +5,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { generateId } from '@/lib/core/utils/uuid'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -18,7 +18,7 @@ import type { MicrosoftGraphDriveItem } from '@/tools/onedrive/types'
* Get files (not folders) from Microsoft OneDrive
*/
export async function GET(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
logger.info(`[${requestId}] OneDrive files request received`)
try {

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -6,6 +5,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { generateId } from '@/lib/core/utils/uuid'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -13,7 +13,7 @@ export const dynamic = 'force-dynamic'
const logger = createLogger('OneDriveFolderAPI')
export async function GET(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
try {
const session = await getSession()

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
@@ -6,6 +5,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { generateId } from '@/lib/core/utils/uuid'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -18,7 +18,7 @@ import type { MicrosoftGraphDriveItem } from '@/tools/onedrive/types'
* Get folders from Microsoft OneDrive
*/
export async function GET(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
try {
const session = await getSession()

View File

@@ -1,9 +1,9 @@
import { randomUUID } from 'crypto'
import type { ItemCreateParams } from '@1password/sdk'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
connectRequest,
createOnePasswordClient,
@@ -28,7 +28,7 @@ const CreateItemSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
@@ -55,7 +55,7 @@ export async function POST(request: NextRequest) {
const parsedFields = params.fields
? (JSON.parse(params.fields) as Array<Record<string, any>>).map((f) => ({
id: f.id || randomUUID().slice(0, 8),
id: f.id || generateId().slice(0, 8),
title: f.label || f.title || '',
fieldType: toSdkFieldType(f.type || 'STRING'),
value: f.value || '',

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { connectRequest, createOnePasswordClient, resolveCredentials } from '../utils'
const logger = createLogger('OnePasswordDeleteItemAPI')
@@ -17,7 +17,7 @@ const DeleteItemSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {

View File

@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
connectRequest,
createOnePasswordClient,
@@ -22,7 +22,7 @@ const GetItemSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {

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