Skip to content

Commit 38c088a

Browse files
authored
v0.7.16: security hardening, db o11y and profiling, settings UI
2 parents 613e8ea + 3766582 commit 38c088a

105 files changed

Lines changed: 3533 additions & 1466 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/commands/add-block.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -600,17 +600,20 @@ export const ServiceV2Block: BlockConfig = {
600600

601601
## Registering Blocks
602602

603-
After creating the block, remind the user to:
604-
1. Import in `apps/sim/blocks/registry.ts`
605-
2. Add to the `registry` object (alphabetically):
603+
After creating the block, remind the user to register it in `apps/sim/blocks/registry-maps.ts` (the data maps live here; `registry.ts` holds only the accessor functions). Add the import and an entry to each map alphabetically:
606604

607605
```typescript
608-
import { ServiceBlock } from '@/blocks/blocks/service'
606+
import { ServiceBlock, ServiceBlockMeta } from '@/blocks/blocks/service'
609607

610-
export const registry: Record<string, BlockConfig> = {
608+
export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
611609
// ... existing blocks ...
612610
service: ServiceBlock,
613611
}
612+
613+
export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
614+
// ... existing metas ...
615+
service: ServiceBlockMeta,
616+
}
614617
```
615618

616619
## Complete Example
@@ -840,7 +843,7 @@ Derive templates from the service's real use cases. Each prompt should name a co
840843
- [ ] Tools.access lists all tool IDs (snake_case)
841844
- [ ] Tools.config.tool returns correct tool ID (snake_case)
842845
- [ ] Outputs match tool outputs
843-
- [ ] Block registered in registry.ts
846+
- [ ] Block + meta registered in registry-maps.ts (`BLOCK_REGISTRY` / `BLOCK_META_REGISTRY`)
844847
- [ ] If icon missing: asked user to provide SVG
845848
- [ ] If triggers exist: `triggers` config set, trigger subBlocks spread
846849
- [ ] Optional/rarely-used fields set to `mode: 'advanced'`

.claude/commands/add-integration.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -364,17 +364,25 @@ export const tools: Record<string, ToolConfig> = {
364364
}
365365
```
366366

367-
### Block Registry (`apps/sim/blocks/registry.ts`)
367+
### Block Registry (`apps/sim/blocks/registry-maps.ts`)
368+
369+
The data maps (`BLOCK_REGISTRY` + `BLOCK_META_REGISTRY`) live in `registry-maps.ts`; `registry.ts` holds only the accessor functions. Add the import and an entry to each map alphabetically:
368370

369371
```typescript
370372
// Add import (alphabetically)
371-
import { {Service}Block } from '@/blocks/blocks/{service}'
373+
import { {Service}Block, {Service}BlockMeta } from '@/blocks/blocks/{service}'
372374

373-
// Add to registry (alphabetically)
374-
export const registry: Record<string, BlockConfig> = {
375+
// Add to the config map (alphabetically)
376+
export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
375377
// ... existing blocks ...
376378
{service}: {Service}Block,
377379
}
380+
381+
// Add to the catalog-meta map (alphabetically)
382+
export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
383+
// ... existing metas ...
384+
{service}: {Service}BlockMeta,
385+
}
378386
```
379387

380388
### Trigger Registry (`apps/sim/triggers/registry.ts`) - If triggers exist
@@ -443,7 +451,7 @@ If creating V2 versions (API-aligned outputs):
443451
- [ ] Configured tools.access with all tool IDs
444452
- [ ] Configured tools.config.tool selector
445453
- [ ] Defined outputs matching tool outputs
446-
- [ ] Registered block in `blocks/registry.ts`
454+
- [ ] Registered block + meta in `blocks/registry-maps.ts` (`BLOCK_REGISTRY` / `BLOCK_META_REGISTRY`)
447455
- [ ] If triggers: set `triggers.enabled` and `triggers.available`
448456
- [ ] If triggers: spread trigger subBlocks with `getTrigger()`
449457
- [ ] Exported `{Service}BlockMeta` with at least 7 templates

.claude/commands/validate-integration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Read **every** file for the integration — do not skip any:
2424
apps/sim/tools/{service}/ # All tool files, types.ts, index.ts
2525
apps/sim/blocks/blocks/{service}.ts # Block definition
2626
apps/sim/tools/registry.ts # Tool registry entries for this service
27-
apps/sim/blocks/registry.ts # Block registry entry for this service
27+
apps/sim/blocks/registry-maps.ts # Block + meta registry entry (BLOCK_REGISTRY / BLOCK_META_REGISTRY)
2828
apps/sim/components/icons.tsx # Icon definition
2929
apps/sim/lib/auth/auth.ts # OAuth config — should use getCanonicalScopesForProvider()
3030
apps/sim/lib/oauth/oauth.ts # OAuth provider config — single source of truth for scopes
@@ -190,7 +190,7 @@ For **each tool** in `tools.access`:
190190
- [ ] `bgColor` uses the service's brand color hex
191191
- [ ] `icon` references the correct icon component from `@/components/icons`
192192
- [ ] `authMode` is set correctly (`AuthMode.OAuth` or `AuthMode.ApiKey`)
193-
- [ ] Block is registered in `blocks/registry.ts` alphabetically
193+
- [ ] Block + meta are registered in `blocks/registry-maps.ts` (`BLOCK_REGISTRY` / `BLOCK_META_REGISTRY`) alphabetically
194194

195195
### BlockMeta
196196
- [ ] `{Service}BlockMeta` is exported in the same file as the block

.claude/rules/sim-integrations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The full authoring instructions — tool/block/icon/trigger scaffolding, SubBloc
1313

1414
## Hard rules (don't get these wrong)
1515

16-
- Tool IDs are `snake_case` (`service_action`). Register tools in `tools/registry.ts`, blocks in `blocks/registry.ts` (alphabetically), triggers in `triggers/registry.ts`.
16+
- Tool IDs are `snake_case` (`service_action`). Register tools in `tools/registry.ts`, blocks in `blocks/registry-maps.ts` (the `BLOCK_REGISTRY` config map + `BLOCK_META_REGISTRY` catalog-meta map, alphabetically`blocks/registry.ts` holds only the accessor functions), triggers in `triggers/registry.ts`.
1717
- Type coercions (`Number()`, etc.) belong in `tools.config.params` (runs at execution, after variable resolution) — never in `tools.config.tool` (runs at serialization; coercing there destroys dynamic `<Block.output>` references).
1818
- `canonicalParamId` must NOT match any subblock's `id`, must be unique per operation/condition context, and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs.
1919
- Blocks must also set the catalog/UI metadata fields `integrationType`, `tags`, `authMode`, `docsLink`, and export a `{Service}BlockMeta` — see the `/add-block` skill's BlockMeta section for details.

.claude/rules/sim-settings-pages.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,46 @@ Adding a new settings page:
101101
Conditional items become array spreads: `...(canManage ? [{…}] : [])`. Never
102102
hand-roll the `<DropdownMenu>` + `<MoreHorizontal>` trigger per page.
103103

104+
## Save / Discard + unsaved-changes guard
105+
106+
Any settings surface with editable state uses **one** shared stack — never
107+
hand-roll a Save button, a Discard button, a `beforeunload`, or an "Unsaved
108+
changes" modal:
109+
110+
- **`SaveDiscardActions`** (`…/components/save-discard-actions/save-discard-actions`)
111+
— the canonical dirty-gated **Discard + Save** chip pair. Renders nothing when
112+
`!dirty`; otherwise a fragment so it composes beside sibling chips (a detail
113+
view's Delete / Remove override, a Share chip). Props: `dirty`, `saving`,
114+
`onSave`, `onDiscard`, `saveDisabled?`, `saveLabel?`, `savingLabel?`. Put it in
115+
the `SettingsPanel actions` slot (top-level pages) or the detail header bar.
116+
- **`useSettingsUnsavedGuard({ isDirty })`** (`…/settings/hooks/use-settings-unsaved-guard`)
117+
— syncs the page's local `isDirty` into the shared `useSettingsDirtyStore` (so
118+
the sidebar's **section-switch** confirm + the centralized `beforeunload` both
119+
apply for free) and returns `{ showUnsavedModal, setShowUnsavedModal, guardBack,
120+
confirmDiscard }` for a detail view's **in-view back** chip.
121+
- **Top-level pages** (whitelabeling, sso): call it **unassigned**
122+
`useSettingsUnsavedGuard({ isDirty: hasChanges })` — they only need the
123+
store-sync; the sidebar/`beforeunload` do the rest.
124+
- **Detail sub-views** (data-retention, access-control group-detail): route the
125+
back chip through `onClick={() => guard.guardBack(closeFn)}` and render the
126+
shared `<UnsavedChangesModal open={guard.showUnsavedModal}
127+
onOpenChange={guard.setShowUnsavedModal} onDiscard={guard.confirmDiscard} />`
128+
(from `@/app/workspace/[workspaceId]/components/credential-detail`). The
129+
in-view header **Discard** chip (via `SaveDiscardActions onDiscard`) is a
130+
*reset to original* — distinct from the back-confirm's discard, which leaves.
131+
- **`useSettingsBeforeUnload`** is mounted **once** in the settings shell
132+
(`settings/[section]/settings.tsx`) — never add a per-page `beforeunload`.
133+
- **Dirty *computation* stays local** (shapes differ: field-compare vs
134+
normalize+stringify) — only how dirty is *consumed* is shared. Derive it (a
135+
`const`/`useMemo`), never store it in `useState`.
136+
- **CRITICAL — rules of hooks:** call `useSettingsUnsavedGuard(...)`
137+
**unconditionally, before every early-return gate** (entitlement / loading /
138+
not-entitled `return <SettingsEmptyState>`). A hook placed after a gate is
139+
skipped on gated renders and crashes.
140+
- The route-based credential detail keeps its own `useUnsavedChangesGuard` (it
141+
guards real `router.push` navigation + browser Back via a history sentinel);
142+
it already shares `UnsavedChangesModal`, so copy stays unified.
143+
104144
## Detail sub-views (the one exception)
105145

106146
A drill-down view reached from a list row (selected MCP server, workflow MCP
@@ -119,5 +159,6 @@ A settings page is design-system-clean when:
119159
- [ ] Header chips are in `actions`; a standalone search is in the `search` prop.
120160
- [ ] Its `NavigationItem` has an accurate, consistent-length `description`.
121161
- [ ] Detail sub-views and entitlement/loading gates keep their own chrome (intentional).
162+
- [ ] If it has editable state: Save/Discard go through `SaveDiscardActions`, dirty is wired via `useSettingsUnsavedGuard` (called before any early-return gate), and there is **no** hand-rolled Save button / `beforeunload` / "Unsaved changes" modal.
122163
- [ ] No business logic, handlers, or conditional rendering changed by the migration.
123164
- [ ] `tsc`, `biome`, and the page's tests pass.

.claude/skills/add-settings-page/SKILL.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,15 @@ Key paths:
2929
bar, scroll region, content column, or title block. Put header buttons in
3030
`actions`, a standalone search in `search={{ value, onChange, placeholder }}`,
3131
and the page content as `children`. Modals go beside the panel inside a `<>`.
32-
4. **Verify:** `cd apps/sim && bunx tsc --noEmit`; `bunx biome check --write <file>`.
32+
4. **If the page has editable state**, wire the shared save/discard stack — put
33+
`SaveDiscardActions` (dirty-gated Discard+Save chips) in `actions`, and call
34+
`useSettingsUnsavedGuard({ isDirty })` **before any early-return gate**.
35+
Detail sub-views additionally route the back chip through
36+
`guard.guardBack(closeFn)` and render the shared `UnsavedChangesModal`. Never
37+
hand-roll a Save button, a `beforeunload`, or an "Unsaved changes" modal —
38+
they're centralized. See the "Save / Discard + unsaved-changes guard" section
39+
in `.claude/rules/sim-settings-pages.md`.
40+
5. **Verify:** `cd apps/sim && bunx tsc --noEmit`; `bunx biome check --write <file>`.
3341

3442
## Mode B — Audit existing settings pages
3543

@@ -44,6 +52,11 @@ For each page component, confirm the checklist in `.claude/rules/sim-settings-pa
4452
`git grep -n "text-\[var(--text-body)\] text-lg" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
4553
3. Confirm each page imports `SettingsPanel` and that its `NavigationItem` has an
4654
accurate `description` of consistent length with its peers.
55+
- Editable pages: confirm Save/Discard go through `SaveDiscardActions` and
56+
dirty is wired via `useSettingsUnsavedGuard` (called before early-return
57+
gates) — flag any hand-rolled Save button, `beforeunload`, or unsaved modal.
58+
`git grep -n "beforeunload" -- 'apps/sim/**/settings/' 'apps/sim/ee/'`
59+
should only hit the centralized `use-settings-before-unload.ts`.
4760
4. When migrating a page, change ONLY the structural shell→`SettingsPanel` swap:
4861
move header chips to `actions`, the standalone search to `search`, delete the
4962
`<h1>` title block, replace the three closing `</div>` (column/scroll/shell)

.cursor/commands/add-block.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -608,17 +608,20 @@ export const ServiceV2Block: BlockConfig = {
608608

609609
## Registering Blocks
610610

611-
After creating the block, remind the user to:
612-
1. Import in `apps/sim/blocks/registry.ts`
613-
2. Add to the `registry` object (alphabetically):
611+
After creating the block, remind the user to register it in `apps/sim/blocks/registry-maps.ts` (the data maps live here; `registry.ts` holds only the accessor functions). Add the import and an entry to each map alphabetically:
614612

615613
```typescript
616-
import { ServiceBlock } from '@/blocks/blocks/service'
614+
import { ServiceBlock, ServiceBlockMeta } from '@/blocks/blocks/service'
617615

618-
export const registry: Record<string, BlockConfig> = {
616+
export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
619617
// ... existing blocks ...
620618
service: ServiceBlock,
621619
}
620+
621+
export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
622+
// ... existing metas ...
623+
service: ServiceBlockMeta,
624+
}
622625
```
623626

624627
## Complete Example
@@ -847,7 +850,7 @@ Derive templates from the service's real use cases. Each prompt should name a co
847850
- [ ] Tools.access lists all tool IDs (snake_case)
848851
- [ ] Tools.config.tool returns correct tool ID (snake_case)
849852
- [ ] Outputs match tool outputs
850-
- [ ] Block registered in registry.ts
853+
- [ ] Block + meta registered in registry-maps.ts (`BLOCK_REGISTRY` / `BLOCK_META_REGISTRY`)
851854
- [ ] If icon missing: asked user to provide SVG
852855
- [ ] If triggers exist: `triggers` config set, trigger subBlocks spread
853856
- [ ] Optional/rarely-used fields set to `mode: 'advanced'`

.cursor/commands/add-integration.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -337,17 +337,25 @@ export const tools: Record<string, ToolConfig> = {
337337
}
338338
```
339339

340-
### Block Registry (`apps/sim/blocks/registry.ts`)
340+
### Block Registry (`apps/sim/blocks/registry-maps.ts`)
341+
342+
The data maps (`BLOCK_REGISTRY` + `BLOCK_META_REGISTRY`) live in `registry-maps.ts`; `registry.ts` holds only the accessor functions. Add the import and an entry to each map alphabetically:
341343

342344
```typescript
343345
// Add import (alphabetically)
344-
import { {Service}Block } from '@/blocks/blocks/{service}'
346+
import { {Service}Block, {Service}BlockMeta } from '@/blocks/blocks/{service}'
345347

346-
// Add to registry (alphabetically)
347-
export const registry: Record<string, BlockConfig> = {
348+
// Add to the config map (alphabetically)
349+
export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
348350
// ... existing blocks ...
349351
{service}: {Service}Block,
350352
}
353+
354+
// Add to the catalog-meta map (alphabetically)
355+
export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
356+
// ... existing metas ...
357+
{service}: {Service}BlockMeta,
358+
}
351359
```
352360

353361
### Trigger Registry (`apps/sim/triggers/registry.ts`) - If triggers exist
@@ -416,7 +424,7 @@ If creating V2 versions (API-aligned outputs):
416424
- [ ] Configured tools.access with all tool IDs
417425
- [ ] Configured tools.config.tool selector
418426
- [ ] Defined outputs matching tool outputs
419-
- [ ] Registered block in `blocks/registry.ts`
427+
- [ ] Registered block + meta in `blocks/registry-maps.ts` (`BLOCK_REGISTRY` / `BLOCK_META_REGISTRY`)
420428
- [ ] If triggers: set `triggers.enabled` and `triggers.available`
421429
- [ ] If triggers: spread trigger subBlocks with `getTrigger()`
422430

.cursor/commands/validate-integration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Read **every** file for the integration — do not skip any:
1919
apps/sim/tools/{service}/ # All tool files, types.ts, index.ts
2020
apps/sim/blocks/blocks/{service}.ts # Block definition
2121
apps/sim/tools/registry.ts # Tool registry entries for this service
22-
apps/sim/blocks/registry.ts # Block registry entry for this service
22+
apps/sim/blocks/registry-maps.ts # Block + meta registry entry (BLOCK_REGISTRY / BLOCK_META_REGISTRY)
2323
apps/sim/components/icons.tsx # Icon definition
2424
apps/sim/lib/auth/auth.ts # OAuth config — should use getCanonicalScopesForProvider()
2525
apps/sim/lib/oauth/oauth.ts # OAuth provider config — single source of truth for scopes
@@ -185,7 +185,7 @@ For **each tool** in `tools.access`:
185185
- [ ] `bgColor` uses the service's brand color hex
186186
- [ ] `icon` references the correct icon component from `@/components/icons`
187187
- [ ] `authMode` is set correctly (`AuthMode.OAuth` or `AuthMode.ApiKey`)
188-
- [ ] Block is registered in `blocks/registry.ts` alphabetically
188+
- [ ] Block + meta are registered in `blocks/registry-maps.ts` (`BLOCK_REGISTRY` / `BLOCK_META_REGISTRY`) alphabetically
189189

190190
### BlockMeta
191191
- [ ] `{Service}BlockMeta` is exported in the same file as the block

.cursor/rules/sim-integrations.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The full authoring instructions — tool/block/icon/trigger scaffolding, SubBloc
1010

1111
## Hard rules (don't get these wrong)
1212

13-
- Tool IDs are `snake_case` (`service_action`). Register tools in `tools/registry.ts`, blocks in `blocks/registry.ts` (alphabetically), triggers in `triggers/registry.ts`.
13+
- Tool IDs are `snake_case` (`service_action`). Register tools in `tools/registry.ts`, blocks in `blocks/registry-maps.ts` (the `BLOCK_REGISTRY` config map + `BLOCK_META_REGISTRY` catalog-meta map, alphabetically — `blocks/registry.ts` holds only the accessor functions), triggers in `triggers/registry.ts`.
1414
- Type coercions (`Number()`, etc.) belong in `tools.config.params` (runs at execution, after variable resolution) — never in `tools.config.tool` (runs at serialization; coercing there destroys dynamic `<Block.output>` references).
1515
- `canonicalParamId` must NOT match any subblock's `id`, must be unique per operation/condition context, and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs.
1616
- Blocks must also set the catalog/UI metadata fields `integrationType`, `tags`, `authMode`, `docsLink`, and export a `{Service}BlockMeta` — see the `/add-block` skill's BlockMeta section for details.

0 commit comments

Comments
 (0)