@liiift-studio/deploy-vercel-from-sanity 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,21 +1,28 @@
1
1
  # deploy-vercel-from-sanity
2
2
 
3
- A [Sanity Studio](https://www.sanity.io) plugin to trigger, monitor, and manage Vercel deployments without leaving your studio.
3
+ **Trigger and monitor Vercel deployments directly from [Sanity Studio](https://www.sanity.io) — no context switching required.**
4
4
 
5
- ![Deploy tool screenshot](./docs/screenshot.png)
5
+ [![npm version](https://img.shields.io/npm/v/@liiift-studio/deploy-vercel-from-sanity)](https://www.npmjs.com/package/@liiift-studio/deploy-vercel-from-sanity)
6
+ [![Sanity v3/v4/v5](https://img.shields.io/badge/sanity-v3%20%7C%20v4%20%7C%20v5-f03e2f)](https://www.sanity.io)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
8
+
9
+ ![Deploy with Vercel — Production and Preview targets with live status](./docs/screenshot.png)
10
+
11
+ ---
6
12
 
7
13
  ## Features
8
14
 
9
- - **Trigger deployments** via Vercel deploy hooks
15
+ - **One-click deploy** trigger Production or Preview builds from inside Sanity Studio
10
16
  - **Live status** with automatic polling — Queued → Building → Ready / Error
11
- - **Build timer** showing elapsed time while a deploy is running
17
+ - **Build timer** showing elapsed time while a deploy is in progress
12
18
  - **Cancel** in-progress deployments
13
19
  - **Copy deployment URL** with one click
14
- - **Inline error log viewer** — see build errors directly in the card without going to Vercel
20
+ - **Inline error log viewer** — see build errors without leaving the studio
15
21
  - **Deployment history** per target
16
- - **"Open in Vercel"** link to the project dashboard
22
+ - **"Open in Vercel"** link to your project dashboard
23
+ - **Multiple targets** — configure Production, Preview, and any number of custom environments
24
+ - **Shared API token** — set it once; all authenticated studio users with editor access or above can deploy
17
25
  - **Responsive grid layout** — 2 columns on desktop, 1 on mobile
18
- - **Shared API token** — set it once, works for all studio users with editor access or above
19
26
 
20
27
  ---
21
28
 
@@ -27,7 +34,7 @@ npm install @liiift-studio/deploy-vercel-from-sanity
27
34
 
28
35
  ---
29
36
 
30
- ## Setup
37
+ ## Quick start
31
38
 
32
39
  ### 1. Add the plugin to your Sanity config
33
40
 
@@ -39,38 +46,22 @@ import { vercelDeploy } from '@liiift-studio/deploy-vercel-from-sanity'
39
46
  export default defineConfig({
40
47
  // ...
41
48
  plugins: [
42
- vercelDeploy(),
43
- // or with a custom label / name:
44
49
  vercelDeploy({ title: 'Deploy', name: 'vercel-deploy' }),
45
50
  ],
46
51
  })
47
52
  ```
48
53
 
49
- ### 2. (Optional) Restrict the tool to editors and above
54
+ ### 2. Connect your Vercel API token
50
55
 
51
- By default the tool is visible to all authenticated users. To hide it from viewers:
52
-
53
- ```ts
54
- // sanity.config.ts
55
- tools: (prev, { currentUser }) => {
56
- const canDeploy = currentUser?.roles?.some(r =>
57
- ['administrator', 'editor'].includes(r.name)
58
- )
59
- return canDeploy ? prev : prev.filter(t => t.name !== 'vercel-deploy')
60
- },
61
- ```
62
-
63
- ### 3. Connect your Vercel API token
64
-
65
- Open the **Deploy** tool in Sanity Studio. Enter a Vercel API token when prompted.
56
+ Open the **Deploy** tab in Sanity Studio and enter a Vercel API token when prompted.
66
57
 
67
58
  To create a token: **vercel.com → Settings → Tokens → Create → Full Account scope**.
68
59
 
69
- The token is stored in a `config.vercelDeploy` document in your dataset and is shared across all authenticated studio users (see [Security](#security)).
60
+ The token is stored in a `config.vercelDeploy` document in your dataset and shared across all authenticated studio users.
70
61
 
71
- ### 4. Create deploy targets
62
+ ### 3. Add a deploy target
72
63
 
73
- Create one or more `vercel_deploy` documents in your dataset. Each represents a deployment environment (e.g. Production, Staging).
64
+ Create one or more `vercel_deploy` documents each represents an environment (Production, Preview, etc.).
74
65
 
75
66
  **Via Sanity CLI:**
76
67
 
@@ -87,11 +78,11 @@ EOF
87
78
 
88
79
  **To get your deploy hook URL:** Vercel Dashboard → Project → Settings → Git → Deploy Hooks → Create Hook.
89
80
 
90
- **Fields on each `vercel_deploy` document:**
81
+ **Available fields on each `vercel_deploy` document:**
91
82
 
92
83
  | Field | Type | Required | Description |
93
84
  |---|---|---|---|
94
- | `name` | `string` | ✓ | Display label shown in the studio |
85
+ | `name` | `string` | ✓ | Display label (e.g. "Production", "Preview") |
95
86
  | `url` | `url` | ✓ | Vercel deploy hook URL |
96
87
  | `teamId` | `string` | | Vercel team ID — required for team-owned projects |
97
88
  | `disableDeleteAction` | `boolean` | | Hides the delete button for this target in the studio UI |
@@ -108,6 +99,22 @@ EOF
108
99
 
109
100
  ---
110
101
 
102
+ ## Restrict access to editors and above
103
+
104
+ By default the Deploy tab is visible to all authenticated users. To hide it from viewers:
105
+
106
+ ```ts
107
+ // sanity.config.ts
108
+ tools: (prev, { currentUser }) => {
109
+ const canDeploy = currentUser?.roles?.some(r =>
110
+ ['administrator', 'editor'].includes(r.name)
111
+ )
112
+ return canDeploy ? prev : prev.filter(t => t.name !== 'vercel-deploy')
113
+ },
114
+ ```
115
+
116
+ ---
117
+
111
118
  ## How it works
112
119
 
113
120
  1. Deploy targets are stored as `vercel_deploy` documents in your Sanity dataset.
@@ -120,25 +127,13 @@ EOF
120
127
 
121
128
  ## Security
122
129
 
123
- ### API token storage
124
-
125
- The Vercel API token is stored in a `config.vercelDeploy` Sanity document, which is readable by **all authenticated studio users**. Anyone with a login to your Sanity project can read the token value via the Sanity API.
126
-
127
- **Recommendations:**
128
- - Audit who has access to your Sanity project at sanity.io → Project → Members.
129
- - If your studio includes untrusted editors, consider a server-side proxy that holds the token outside Sanity and exposes only a scoped deploy endpoint.
130
-
131
- ### Deploy hook URL validation
132
-
133
- `triggerDeploy` validates that the hook URL matches `api.vercel.com/v1/integrations/deploy/` before making the request. This prevents SSRF if a `vercel_deploy` document is created or modified outside the Studio's schema validation.
134
-
135
- ### External links
130
+ **API token storage** — The Vercel API token is stored in a `config.vercelDeploy` Sanity document, readable by all authenticated studio users. Audit who has access to your Sanity project at sanity.io → Project → Members. If your studio includes untrusted editors, consider a server-side proxy that holds the token and exposes only a scoped deploy endpoint.
136
131
 
137
- All external links use `target="_blank" rel="noreferrer"` and are validated through `safeHref()` before rendering, blocking `javascript:` injection from a compromised API response.
132
+ **Deploy hook URL validation** `triggerDeploy` validates that the hook URL matches `api.vercel.com/v1/integrations/deploy/` before making the request, preventing SSRF from a tampered document.
138
133
 
139
- ### GROQ queries
134
+ **External links** — All external links use `target="_blank" rel="noreferrer"` and are validated through `safeHref()` before rendering, blocking `javascript:` injection from a compromised API response.
140
135
 
141
- All GROQ queries in this plugin are static strings — no user input is interpolated, so there is no GROQ injection risk.
136
+ **GROQ queries** — All GROQ queries in this plugin are static strings — no user input is interpolated.
142
137
 
143
138
  ---
144
139
 
@@ -146,13 +141,13 @@ All GROQ queries in this plugin are static strings — no user input is interpol
146
141
 
147
142
  - Sanity Studio v3, v4, or v5
148
143
  - React 18 or 19
149
- - A Vercel account with at least one project configured with a deploy hook
144
+ - A Vercel account with at least one project and a deploy hook configured
150
145
 
151
146
  ---
152
147
 
153
148
  ## Contributing
154
149
 
155
- Issues and pull requests are welcome at [github.com/Liiift-Studio/Deploy-Vercel-from-Sanity](https://github.com/Liiift-Studio/Deploy-Vercel-from-Sanity).
150
+ Issues and pull requests welcome at [github.com/Liiift-Studio/Deploy-Vercel-from-Sanity](https://github.com/Liiift-Studio/Deploy-Vercel-from-Sanity).
156
151
 
157
152
  ---
158
153
 
package/dist/index.js CHANGED
@@ -412,8 +412,20 @@ function DeployItem({ target, token, onDelete, onEdit }) {
412
412
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Spinner, { muted: true }),
413
413
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Badge, { tone: "caution", padding: 2, children: "Triggering\u2026" })
414
414
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatusBadge, { state: latest?.state, showSpinner: true }),
415
+ isActiveState(latest?.state) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
416
+ import_ui3.Button,
417
+ {
418
+ text: "Cancel",
419
+ mode: "ghost",
420
+ tone: "critical",
421
+ loading: canceling,
422
+ disabled: canceling,
423
+ onClick: cancel,
424
+ style: { cursor: "pointer" }
425
+ }
426
+ ),
415
427
  isActive && elapsed > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "center", gap: 1, children: [
416
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons2.ClockIcon, {}),
428
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Spinner, { muted: true }),
417
429
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, children: formatDuration(elapsed) })
418
430
  ] }) : !isActive && deployedAt ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, children: deployedAt }) : null
419
431
  ] })
@@ -640,44 +652,30 @@ function DeployItem({ target, token, onDelete, onEdit }) {
640
652
  ] }) })
641
653
  ] })
642
654
  ] }),
643
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
655
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
644
656
  import_ui3.Flex,
645
657
  {
646
658
  direction: "column",
647
659
  gap: 2,
648
660
  className: "dvfs-deploy-col",
649
661
  style: { flexShrink: 0, alignSelf: "stretch" },
650
- children: [
651
- isActiveState(latest?.state) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
652
- import_ui3.Button,
653
- {
654
- text: "Cancel",
655
- mode: "ghost",
656
- tone: "critical",
657
- loading: canceling,
658
- disabled: canceling,
659
- onClick: cancel,
660
- style: { cursor: "pointer" }
661
- }
662
- ),
663
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
664
- import_ui3.Button,
665
- {
666
- text: "Deploy",
667
- tone: "primary",
668
- loading: triggering,
669
- disabled: isActive,
670
- onClick: deploy,
671
- style: {
672
- flex: 1,
673
- borderRadius: 0,
674
- borderTopRightRadius: 3,
675
- borderBottomRightRadius: 3,
676
- cursor: "pointer"
677
- }
662
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
663
+ import_ui3.Button,
664
+ {
665
+ text: "Deploy",
666
+ tone: "primary",
667
+ loading: triggering,
668
+ disabled: isActive,
669
+ onClick: deploy,
670
+ style: {
671
+ flex: 1,
672
+ borderRadius: 0,
673
+ borderTopRightRadius: 3,
674
+ borderBottomRightRadius: 3,
675
+ cursor: "pointer"
678
676
  }
679
- )
680
- ]
677
+ }
678
+ )
681
679
  }
682
680
  )
683
681
  ] }) }),
package/dist/index.mjs CHANGED
@@ -433,8 +433,20 @@ function DeployItem({ target, token, onDelete, onEdit }) {
433
433
  /* @__PURE__ */ jsx3(Spinner3, { muted: true }),
434
434
  /* @__PURE__ */ jsx3(Badge3, { tone: "caution", padding: 2, children: "Triggering\u2026" })
435
435
  ] }) : /* @__PURE__ */ jsx3(StatusBadge, { state: latest?.state, showSpinner: true }),
436
+ isActiveState(latest?.state) && /* @__PURE__ */ jsx3(
437
+ Button2,
438
+ {
439
+ text: "Cancel",
440
+ mode: "ghost",
441
+ tone: "critical",
442
+ loading: canceling,
443
+ disabled: canceling,
444
+ onClick: cancel,
445
+ style: { cursor: "pointer" }
446
+ }
447
+ ),
436
448
  isActive && elapsed > 0 ? /* @__PURE__ */ jsxs3(Flex3, { align: "center", gap: 1, children: [
437
- /* @__PURE__ */ jsx3(ClockIcon, {}),
449
+ /* @__PURE__ */ jsx3(Spinner3, { muted: true }),
438
450
  /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: formatDuration(elapsed) })
439
451
  ] }) : !isActive && deployedAt ? /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: deployedAt }) : null
440
452
  ] })
@@ -661,44 +673,30 @@ function DeployItem({ target, token, onDelete, onEdit }) {
661
673
  ] }) })
662
674
  ] })
663
675
  ] }),
664
- /* @__PURE__ */ jsxs3(
676
+ /* @__PURE__ */ jsx3(
665
677
  Flex3,
666
678
  {
667
679
  direction: "column",
668
680
  gap: 2,
669
681
  className: "dvfs-deploy-col",
670
682
  style: { flexShrink: 0, alignSelf: "stretch" },
671
- children: [
672
- isActiveState(latest?.state) && /* @__PURE__ */ jsx3(
673
- Button2,
674
- {
675
- text: "Cancel",
676
- mode: "ghost",
677
- tone: "critical",
678
- loading: canceling,
679
- disabled: canceling,
680
- onClick: cancel,
681
- style: { cursor: "pointer" }
682
- }
683
- ),
684
- /* @__PURE__ */ jsx3(
685
- Button2,
686
- {
687
- text: "Deploy",
688
- tone: "primary",
689
- loading: triggering,
690
- disabled: isActive,
691
- onClick: deploy,
692
- style: {
693
- flex: 1,
694
- borderRadius: 0,
695
- borderTopRightRadius: 3,
696
- borderBottomRightRadius: 3,
697
- cursor: "pointer"
698
- }
683
+ children: /* @__PURE__ */ jsx3(
684
+ Button2,
685
+ {
686
+ text: "Deploy",
687
+ tone: "primary",
688
+ loading: triggering,
689
+ disabled: isActive,
690
+ onClick: deploy,
691
+ style: {
692
+ flex: 1,
693
+ borderRadius: 0,
694
+ borderTopRightRadius: 3,
695
+ borderBottomRightRadius: 3,
696
+ cursor: "pointer"
699
697
  }
700
- )
701
- ]
698
+ }
699
+ )
702
700
  }
703
701
  )
704
702
  ] }) }),
package/package.json CHANGED
@@ -1,15 +1,21 @@
1
1
  {
2
2
  "name": "@liiift-studio/deploy-vercel-from-sanity",
3
- "version": "0.2.0",
4
- "description": "Sanity Studio v5 plugin — trigger and monitor Vercel deployments with full status, history, and build logs",
3
+ "version": "0.2.2",
4
+ "description": "Sanity Studio plugin — trigger and monitor Vercel deployments with full status, history, and build logs. Supports v3, v4, and v5.",
5
5
  "license": "MIT",
6
6
  "author": "Liiift Studio",
7
7
  "keywords": [
8
8
  "sanity",
9
9
  "sanity-plugin",
10
+ "sanity-studio",
10
11
  "vercel",
12
+ "vercel-deploy",
11
13
  "deploy",
12
- "deployment"
14
+ "deployment",
15
+ "deploy-hook",
16
+ "ci-cd",
17
+ "build-trigger",
18
+ "cms-deploy"
13
19
  ],
14
20
  "main": "./dist/index.js",
15
21
  "module": "./dist/index.mjs",
@@ -33,7 +39,7 @@
33
39
  },
34
40
  "peerDependencies": {
35
41
  "react": ">=18",
36
- "sanity": ">=5"
42
+ "sanity": ">=3"
37
43
  },
38
44
  "devDependencies": {
39
45
  "@sanity/icons": "^3",
@@ -200,9 +200,20 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
200
200
  ) : (
201
201
  <StatusBadge state={latest?.state} showSpinner />
202
202
  )}
203
+ {isActiveState(latest?.state) && (
204
+ <Button
205
+ text="Cancel"
206
+ mode="ghost"
207
+ tone="critical"
208
+ loading={canceling}
209
+ disabled={canceling}
210
+ onClick={cancel}
211
+ style={{ cursor: 'pointer' }}
212
+ />
213
+ )}
203
214
  {isActive && elapsed > 0 ? (
204
215
  <Flex align="center" gap={1}>
205
- <ClockIcon />
216
+ <Spinner muted />
206
217
  <Text size={1} muted>{formatDuration(elapsed)}</Text>
207
218
  </Flex>
208
219
  ) : (!isActive && deployedAt) ? (
@@ -511,17 +522,6 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
511
522
  className="dvfs-deploy-col"
512
523
  style={{ flexShrink: 0, alignSelf: 'stretch' }}
513
524
  >
514
- {isActiveState(latest?.state) && (
515
- <Button
516
- text="Cancel"
517
- mode="ghost"
518
- tone="critical"
519
- loading={canceling}
520
- disabled={canceling}
521
- onClick={cancel}
522
- style={{ cursor: 'pointer' }}
523
- />
524
- )}
525
525
  <Button
526
526
  text="Deploy"
527
527
  tone="primary"