@liiift-studio/deploy-vercel-from-sanity 1.2.0 → 1.2.1

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
@@ -188,7 +188,7 @@ If "No stderr or stdout was captured" appears, the build may have failed before
188
188
 
189
189
  ## Security
190
190
 
191
- **API token storage** — The Vercel API token is stored in cleartext in a `config.vercelDeploy` document of type `vercelDeploy.config`. Sanity has no per-document access control at this tier, so **the token is readable by anyone who can read the dataset** — and if the dataset is public, which is the usual setup for a statically generated front-end, that includes unauthenticated requests to the public GROQ API. A `viewer`-role member who cannot write a single document can also read it. Note that a **Full Account** scoped token can read and write your entire Vercel account, so anyone with studio access can read a credential that grants broad Vercel access — treat the token accordingly. The document type is registered with the plugin, so it is visible in Structure and can be deleted from the Studio to revoke the stored token. Audit who has access to your Sanity project at sanity.io → Project → Members, and do not store a Full Account token in a public dataset. If your studio includes untrusted editors, consider a server-side proxy that holds the token and exposes only a scoped deploy endpoint.
191
+ **API token storage** — The Vercel API token is stored in cleartext in a `config.vercelDeploy` document of type `vercelDeploy.config`. Sanity has no per-document access control at this tier, so **the token is readable by anyone who can read the dataset** — and if the dataset is public, which is the usual setup for a statically generated front-end, that includes unauthenticated requests to the public GROQ API. A `viewer`-role member who cannot write a single document can also read it. Note that a **Full Account** scoped token can read and write your entire Vercel account, so anyone with studio access can read a credential that grants broad Vercel access — treat the token accordingly. The document type is deliberately **not** registered in the schema registering it would list a "Vercel Deploy Configuration" entry in the Structure sidebar for every editor. Revoke the stored token from the plugin instead: **Deploy → Token connected → Remove token**. Audit who has access to your Sanity project at sanity.io → Project → Members, and do not store a Full Account token in a public dataset. If your studio includes untrusted editors, consider a server-side proxy that holds the token and exposes only a scoped deploy endpoint.
192
192
 
193
193
  **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.
194
194
 
package/dist/index.d.mts CHANGED
@@ -89,38 +89,6 @@ declare const vercelDeploySchema: {
89
89
  };
90
90
  };
91
91
 
92
- /**
93
- * Registers the document the plugin stores its Vercel token in.
94
- *
95
- * The document was previously written without a registered type, which made it
96
- * invisible to Structure, absent from schema extraction, and impossible to
97
- * inspect or revoke from inside the Studio. Registering it does not change where
98
- * the token lives — see the security note in the README about dataset read
99
- * access — but it does make the credential visible and removable.
100
- */
101
- declare const vercelConfigSchema: {
102
- name: "vercelDeploy.config";
103
- title: string;
104
- type: "document";
105
- fields: {
106
- name: "accessToken";
107
- title: string;
108
- type: "string";
109
- description: string;
110
- }[];
111
- preview: {
112
- select: {
113
- token: string;
114
- };
115
- prepare: ({ token }: {
116
- token?: string;
117
- }) => {
118
- title: string;
119
- subtitle: string;
120
- };
121
- };
122
- };
123
-
124
92
  /**
125
93
  * Sanity Studio plugin — trigger and monitor Vercel deployments.
126
94
  *
@@ -141,4 +109,4 @@ declare const vercelConfigSchema: {
141
109
  */
142
110
  declare const vercelDeploy: sanity.Plugin<void | VercelDeployPluginConfig>;
143
111
 
144
- export { type DeployTarget, type VercelDeployPluginConfig, type VercelDeployState, type VercelDeployment, vercelConfigSchema, vercelDeploy, vercelDeploySchema };
112
+ export { type DeployTarget, type VercelDeployPluginConfig, type VercelDeployState, type VercelDeployment, vercelDeploy, vercelDeploySchema };
package/dist/index.d.ts CHANGED
@@ -89,38 +89,6 @@ declare const vercelDeploySchema: {
89
89
  };
90
90
  };
91
91
 
92
- /**
93
- * Registers the document the plugin stores its Vercel token in.
94
- *
95
- * The document was previously written without a registered type, which made it
96
- * invisible to Structure, absent from schema extraction, and impossible to
97
- * inspect or revoke from inside the Studio. Registering it does not change where
98
- * the token lives — see the security note in the README about dataset read
99
- * access — but it does make the credential visible and removable.
100
- */
101
- declare const vercelConfigSchema: {
102
- name: "vercelDeploy.config";
103
- title: string;
104
- type: "document";
105
- fields: {
106
- name: "accessToken";
107
- title: string;
108
- type: "string";
109
- description: string;
110
- }[];
111
- preview: {
112
- select: {
113
- token: string;
114
- };
115
- prepare: ({ token }: {
116
- token?: string;
117
- }) => {
118
- title: string;
119
- subtitle: string;
120
- };
121
- };
122
- };
123
-
124
92
  /**
125
93
  * Sanity Studio plugin — trigger and monitor Vercel deployments.
126
94
  *
@@ -141,4 +109,4 @@ declare const vercelConfigSchema: {
141
109
  */
142
110
  declare const vercelDeploy: sanity.Plugin<void | VercelDeployPluginConfig>;
143
111
 
144
- export { type DeployTarget, type VercelDeployPluginConfig, type VercelDeployState, type VercelDeployment, vercelConfigSchema, vercelDeploy, vercelDeploySchema };
112
+ export { type DeployTarget, type VercelDeployPluginConfig, type VercelDeployState, type VercelDeployment, vercelDeploy, vercelDeploySchema };
package/dist/index.js CHANGED
@@ -30,12 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- vercelConfigSchema: () => vercelConfigSchema,
34
33
  vercelDeploy: () => vercelDeploy,
35
34
  vercelDeploySchema: () => vercelDeploySchema
36
35
  });
37
36
  module.exports = __toCommonJS(index_exports);
38
- var import_sanity6 = require("sanity");
37
+ var import_sanity5 = require("sanity");
39
38
 
40
39
  // src/icons.tsx
41
40
  var import_react = require("react");
@@ -45,10 +44,18 @@ var sanityUi = __toESM(require("@sanity/ui"));
45
44
  var sanityIcons = __toESM(require("@sanity/icons"));
46
45
  var UI = sanityUi;
47
46
  var ICONS = sanityIcons;
48
- function resolveExport(ns, name) {
47
+ function resolveComponent(ns, name) {
49
48
  const value = ns[name];
50
49
  return typeof value === "function" || typeof value === "object" && value !== null ? value : void 0;
51
50
  }
51
+ function resolveFunction(ns, name) {
52
+ const value = ns[name];
53
+ return typeof value === "function" ? value : void 0;
54
+ }
55
+ function resolveRecord(ns, name) {
56
+ const value = ns[name];
57
+ return typeof value === "object" && value !== null ? value : void 0;
58
+ }
52
59
  var STACK_USES_GAP = typeof UI.Stack === "function";
53
60
 
54
61
  // src/icons.tsx
@@ -57,11 +64,11 @@ var GLYPH = { width: "1em", height: "1em", viewBox: "0 0 25 25", fill: "none" };
57
64
  var MissingIcon = (0, import_react.forwardRef)(function MissingIcon2(props, ref) {
58
65
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { ...GLYPH, xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", focusable: "false", ...props, ref });
59
66
  });
60
- var SYMBOL_MAP = resolveExport(ICONS, "icons");
67
+ var SYMBOL_MAP = resolveRecord(ICONS, "icons");
61
68
  function resolveIcon(name, symbol) {
62
- const named = resolveExport(ICONS, name);
69
+ const named = resolveComponent(ICONS, name);
63
70
  if (named) return named;
64
- const Icon = resolveExport(ICONS, "Icon");
71
+ const Icon = resolveComponent(ICONS, "Icon");
65
72
  if (!Icon) return MissingIcon;
66
73
  if (SYMBOL_MAP && !(symbol in SYMBOL_MAP)) return MissingIcon;
67
74
  const Resolved = (0, import_react.forwardRef)(function SanityIcon(props, ref) {
@@ -99,21 +106,43 @@ var import_react2 = require("react");
99
106
  var import_jsx_runtime2 = require("react/jsx-runtime");
100
107
  function domFallback(tag) {
101
108
  const Fallback = (0, import_react2.forwardRef)(function SanityUiFallback(props, ref) {
102
- const { children, style, id, className, onClick, href, title, ...rest } = props;
109
+ const { children, style, id, className, onClick, href, title, as, ...rest } = props;
110
+ const element = typeof as === "string" ? as : tag;
103
111
  const passthrough = { style, id, className, onClick, href, title, ref };
104
- for (const key of ["role", "type", "value", "checked", "placeholder", "disabled", "onChange", "onKeyDown"]) {
112
+ for (const key of [
113
+ "role",
114
+ "type",
115
+ "value",
116
+ "checked",
117
+ "placeholder",
118
+ "disabled",
119
+ "tabIndex",
120
+ "onChange",
121
+ "onKeyDown",
122
+ "onFocus",
123
+ "onBlur",
124
+ "onMouseEnter",
125
+ "onMouseLeave",
126
+ "htmlFor",
127
+ "target",
128
+ "rel",
129
+ "name",
130
+ "autoComplete",
131
+ "required",
132
+ "readOnly"
133
+ ]) {
105
134
  if (key in rest) passthrough[key] = rest[key];
106
135
  }
107
136
  for (const key of Object.keys(rest)) {
108
137
  if (key.startsWith("aria-") || key.startsWith("data-")) passthrough[key] = rest[key];
109
138
  }
110
- return (0, import_react2.createElement)(tag, passthrough, children);
139
+ return (0, import_react2.createElement)(element, passthrough, children);
111
140
  });
112
141
  Fallback.displayName = `SanityUiFallback(${tag})`;
113
142
  return Fallback;
114
143
  }
115
144
  function primitive(name, tag) {
116
- return resolveExport(UI, name) ?? domFallback(tag);
145
+ return resolveComponent(UI, name) ?? domFallback(tag);
117
146
  }
118
147
  var Box = primitive("Box", "div");
119
148
  var Card = primitive("Card", "div");
@@ -172,11 +201,19 @@ function scheduleDismiss(id, status) {
172
201
  }
173
202
  function pushLocalToast(params) {
174
203
  const toast = { ...params, id: nextToastId++ };
175
- localToasts = [...localToasts, toast].slice(-MAX_VISIBLE_TOASTS);
204
+ const next = [...localToasts, toast];
205
+ for (const dropped of next.slice(0, Math.max(0, next.length - MAX_VISIBLE_TOASTS))) {
206
+ const timer = dismissTimers.get(dropped.id);
207
+ if (timer) {
208
+ clearTimeout(timer);
209
+ dismissTimers.delete(dropped.id);
210
+ }
211
+ }
212
+ localToasts = next.slice(-MAX_VISIBLE_TOASTS);
176
213
  emitToasts();
177
214
  scheduleDismiss(toast.id, toast.status);
178
215
  }
179
- var installedUseToast = resolveExport(UI, "useToast");
216
+ var installedUseToast = resolveFunction(UI, "useToast");
180
217
  var localToaster = { push: pushLocalToast };
181
218
  function useToast() {
182
219
  const real = installedUseToast;
@@ -193,6 +230,10 @@ function ToastViewport() {
193
230
  toastListeners.delete(setToasts);
194
231
  };
195
232
  }, []);
233
+ const resume = (0, import_react3.useCallback)((toast, el) => {
234
+ if (el && el.contains(document.activeElement)) return;
235
+ scheduleDismiss(toast.id, toast.status);
236
+ }, []);
196
237
  const hold = (0, import_react3.useCallback)((id) => {
197
238
  const timer = dismissTimers.get(id);
198
239
  if (timer) {
@@ -204,9 +245,8 @@ function ToastViewport() {
204
245
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
205
246
  Box,
206
247
  {
207
- role: "status",
208
- "aria-live": "polite",
209
- "aria-atomic": false,
248
+ role: "log",
249
+ "aria-label": "Deployment notifications",
210
250
  style: {
211
251
  position: "fixed",
212
252
  bottom: 16,
@@ -221,11 +261,12 @@ function ToastViewport() {
221
261
  padding: 3,
222
262
  radius: 2,
223
263
  shadow: 3,
224
- tone: LOCAL_TOAST_TONE[toast.status ?? "info"] ?? "primary",
264
+ tone: LOCAL_TOAST_TONE[toast.status ?? "info"],
225
265
  style: { pointerEvents: "auto" },
226
266
  onMouseEnter: () => hold(toast.id),
227
267
  onFocusCapture: () => hold(toast.id),
228
- onMouseLeave: () => scheduleDismiss(toast.id, toast.status),
268
+ onMouseLeave: (e) => resume(toast, e.currentTarget),
269
+ onBlurCapture: (e) => resume(toast, e.currentTarget),
229
270
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Flex, { align: "flex-start", gap: 3, children: [
230
271
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Stack, { space: 2, flex: 1, children: [
231
272
  toast.title && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { size: 1, weight: "semibold", children: toast.title }),
@@ -253,48 +294,47 @@ function ToastViewport() {
253
294
  // src/compat/tooltip.tsx
254
295
  var import_react4 = require("react");
255
296
  var import_jsx_runtime4 = require("react/jsx-runtime");
256
- var InstalledTooltip = resolveExport(UI, "Tooltip");
297
+ var InstalledTooltip = resolveComponent(UI, "Tooltip");
257
298
  function Tooltip({ text, children }) {
258
299
  const id = (0, import_react4.useId)();
259
300
  const [visible, setVisible] = (0, import_react4.useState)(false);
260
301
  if (InstalledTooltip) {
261
302
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(InstalledTooltip, { content: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box, { padding: 2, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { size: 1, children: text }) }), portal: true, children });
262
303
  }
304
+ const described = visible && (0, import_react4.isValidElement)(children) ? (0, import_react4.cloneElement)(children, { "aria-describedby": id }) : children;
263
305
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
264
306
  "span",
265
307
  {
266
308
  style: { position: "relative", display: "inline-flex" },
267
- "aria-describedby": id,
268
309
  onMouseEnter: () => setVisible(true),
269
310
  onMouseLeave: () => setVisible(false),
270
311
  onFocusCapture: () => setVisible(true),
271
312
  onBlurCapture: () => setVisible(false),
272
313
  onKeyDown: (e) => {
273
- if (e.key === "Escape") setVisible(false);
314
+ if (e.key === "Escape" && visible) {
315
+ e.stopPropagation();
316
+ setVisible(false);
317
+ }
274
318
  },
275
319
  children: [
276
- children,
277
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
278
- Card,
320
+ described,
321
+ visible && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
322
+ "span",
279
323
  {
280
324
  id,
281
325
  role: "tooltip",
282
- radius: 2,
283
- shadow: 2,
284
- padding: 2,
285
326
  style: {
286
327
  position: "absolute",
287
328
  bottom: "100%",
288
329
  left: "50%",
289
330
  transform: "translateX(-50%)",
290
- marginBottom: 4,
331
+ // No gap between trigger and tooltip, so the pointer can travel onto it
332
+ // without leaving the hover target (WCAG 1.4.13 Hoverable).
333
+ paddingBottom: 4,
291
334
  whiteSpace: "nowrap",
292
- pointerEvents: "none",
293
- zIndex: 1e3,
294
- // Kept in the accessibility tree when hidden so `aria-describedby` still resolves.
295
- visibility: visible ? "visible" : "hidden"
335
+ zIndex: 1e3
296
336
  },
297
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { size: 1, children: text })
337
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Card, { radius: 2, shadow: 2, padding: 2, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { size: 1, children: text }) })
298
338
  }
299
339
  )
300
340
  ]
@@ -305,9 +345,9 @@ function Tooltip({ text, children }) {
305
345
  // src/compat/menu.tsx
306
346
  var import_react5 = require("react");
307
347
  var import_jsx_runtime5 = require("react/jsx-runtime");
308
- var InstalledMenuButton = resolveExport(UI, "MenuButton");
309
- var InstalledMenu = resolveExport(UI, "Menu");
310
- var InstalledMenuItem = resolveExport(UI, "MenuItem");
348
+ var InstalledMenuButton = resolveComponent(UI, "MenuButton");
349
+ var InstalledMenu = resolveComponent(UI, "Menu");
350
+ var InstalledMenuItem = resolveComponent(UI, "MenuItem");
311
351
  var INSTALLED_MENU = InstalledMenuButton && InstalledMenu && InstalledMenuItem ? { MenuButton: InstalledMenuButton, Menu: InstalledMenu, MenuItem: InstalledMenuItem } : null;
312
352
  var itemKey = (item, index) => item.key ?? `${index}-${item.text}`;
313
353
  function ActionMenu({ id, label, items, buttonIcon }) {
@@ -335,7 +375,10 @@ function ActionMenu({ id, label, items, buttonIcon }) {
335
375
  (0, import_react5.useEffect)(() => {
336
376
  if (INSTALLED_MENU || !open) return;
337
377
  const onPointerDown = (e) => {
338
- if (!wrapRef.current?.contains(e.target)) setOpen(false);
378
+ if (wrapRef.current?.contains(e.target)) return;
379
+ const hadFocus = wrapRef.current?.contains(document.activeElement);
380
+ setOpen(false);
381
+ if (hadFocus) triggerRef.current?.focus();
339
382
  };
340
383
  document.addEventListener("mousedown", onPointerDown);
341
384
  return () => document.removeEventListener("mousedown", onPointerDown);
@@ -373,7 +416,20 @@ function ActionMenu({ id, label, items, buttonIcon }) {
373
416
  return;
374
417
  }
375
418
  if (e.key === "Tab") {
376
- close(false);
419
+ triggerRef.current?.focus();
420
+ setOpen(false);
421
+ return;
422
+ }
423
+ if (e.key === "Enter" || e.key === " ") {
424
+ e.preventDefault();
425
+ const item = shownItems[activeIndex];
426
+ if (item) {
427
+ if (item.href) {
428
+ itemRefs.current[activeIndex]?.click();
429
+ } else {
430
+ runAction(item);
431
+ }
432
+ }
377
433
  return;
378
434
  }
379
435
  if (e.key === "ArrowDown") {
@@ -448,7 +504,6 @@ function ActionMenu({ id, label, items, buttonIcon }) {
448
504
  ref: (el) => {
449
505
  itemRefs.current[i] = el;
450
506
  },
451
- onMouseEnter: () => setActiveIndex(i),
452
507
  style: {
453
508
  display: "block",
454
509
  width: "100%",
@@ -473,15 +528,17 @@ function ActionMenu({ id, label, items, buttonIcon }) {
473
528
  href: item.href,
474
529
  target: "_blank",
475
530
  rel: "noreferrer",
476
- onClick: () => close(false),
531
+ onClick: () => {
532
+ setOpen(false);
533
+ triggerRef.current?.focus();
534
+ },
477
535
  children: content
478
536
  },
479
537
  itemKey(item, i)
480
538
  ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
481
- "button",
539
+ "div",
482
540
  {
483
541
  ...shared,
484
- type: "button",
485
542
  onClick: () => runAction(item),
486
543
  children: content
487
544
  },
@@ -495,7 +552,7 @@ function ActionMenu({ id, label, items, buttonIcon }) {
495
552
 
496
553
  // src/compat/code.tsx
497
554
  var import_jsx_runtime6 = require("react/jsx-runtime");
498
- var InstalledCode = resolveExport(UI, "Code");
555
+ var InstalledCode = resolveComponent(UI, "Code");
499
556
  var FALLBACK_CODE_STYLE = {
500
557
  display: "block",
501
558
  fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
@@ -672,6 +729,7 @@ function StatusBadge({ state, showSpinner }) {
672
729
  var import_react6 = require("react");
673
730
  var import_jsx_runtime8 = require("react/jsx-runtime");
674
731
  function DeployHistory({ target, token, onClose }) {
732
+ const dialogId = (0, import_react6.useId)();
675
733
  const [deployments, setDeployments] = (0, import_react6.useState)([]);
676
734
  const [loading, setLoading] = (0, import_react6.useState)(true);
677
735
  const [error, setError] = (0, import_react6.useState)(null);
@@ -695,7 +753,7 @@ function DeployHistory({ target, token, onClose }) {
695
753
  Dialog,
696
754
  {
697
755
  header: `${target.name} \u2014 Deployment History`,
698
- id: "deploy-history",
756
+ id: dialogId,
699
757
  onClose,
700
758
  width: 2,
701
759
  footer: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Box, { padding: 3, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { text: "Close", icon: CloseIcon, mode: "ghost", onClick: onClose }) }),
@@ -783,8 +841,11 @@ function DeployItem({ target, token, onDelete, onEdit }) {
783
841
  const triggeredFromUidRef = (0, import_react7.useRef)(void 0);
784
842
  const requestSeqRef = (0, import_react7.useRef)(0);
785
843
  const mountedRef = (0, import_react7.useRef)(true);
786
- (0, import_react7.useEffect)(() => () => {
787
- mountedRef.current = false;
844
+ (0, import_react7.useEffect)(() => {
845
+ mountedRef.current = true;
846
+ return () => {
847
+ mountedRef.current = false;
848
+ };
788
849
  }, []);
789
850
  const latest = deployments[0];
790
851
  const isPending = pendingSince !== null;
@@ -1010,17 +1071,36 @@ function DeployItem({ target, token, onDelete, onEdit }) {
1010
1071
  formatDuration(Math.floor((latest.ready - latest.created) / 1e3)),
1011
1072
  " to build"
1012
1073
  ] }),
1013
- latest?.url && latest.state === "READY" && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_jsx_runtime9.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Tooltip, { text: copied ? "Copied!" : "Copy URL", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1014
- Button,
1015
- {
1016
- mode: "ghost",
1017
- icon: copied ? CheckmarkIcon : CopyIcon,
1018
- padding: 1,
1019
- tone: copied ? "positive" : "default",
1020
- onClick: copyUrl,
1021
- style: { cursor: "pointer" }
1022
- }
1023
- ) }) })
1074
+ latest?.url && latest.state === "READY" && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
1075
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Tooltip, { text: copied ? "Copied" : "Copy deployment URL", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1076
+ Button,
1077
+ {
1078
+ mode: "ghost",
1079
+ icon: copied ? CheckmarkIcon : CopyIcon,
1080
+ padding: 1,
1081
+ tone: copied ? "positive" : "default",
1082
+ onClick: copyUrl,
1083
+ "aria-label": copied ? "Deployment URL copied" : "Copy deployment URL",
1084
+ style: { cursor: "pointer" }
1085
+ }
1086
+ ) }),
1087
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1088
+ "span",
1089
+ {
1090
+ role: "status",
1091
+ "aria-live": "polite",
1092
+ style: {
1093
+ position: "absolute",
1094
+ width: 1,
1095
+ height: 1,
1096
+ overflow: "hidden",
1097
+ clip: "rect(0 0 0 0)",
1098
+ whiteSpace: "nowrap"
1099
+ },
1100
+ children: copied ? "Deployment URL copied to clipboard" : ""
1101
+ }
1102
+ )
1103
+ ] })
1024
1104
  ] }),
1025
1105
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(Flex, { align: "center", gap: 2, wrap: "wrap", children: [
1026
1106
  commitMsg && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
@@ -1071,7 +1151,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
1071
1151
  deployError && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Card, { tone: "critical", padding: 2, radius: 2, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Text, { size: 1, children: deployError }) })
1072
1152
  ] })
1073
1153
  ] }),
1074
- pollError && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Card, { tone: "caution", padding: 3, radius: 2, role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(Flex, { align: "center", gap: 2, children: [
1154
+ pollError && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Card, { tone: "caution", padding: 3, radius: 2, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(Flex, { align: "center", gap: 2, children: [
1075
1155
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(WarningOutlineIcon, { "aria-hidden": "true" }),
1076
1156
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(Text, { size: 1, children: [
1077
1157
  "Status updates paused \u2014 ",
@@ -1183,12 +1263,14 @@ var import_react8 = require("react");
1183
1263
  var import_sanity = require("sanity");
1184
1264
  var import_jsx_runtime10 = require("react/jsx-runtime");
1185
1265
  var TOKEN_DOC_ID = "config.vercelDeploy";
1186
- function TokenSetup({ onSaved, onCancel }) {
1266
+ function TokenSetup({ onSaved, onCancel, hasToken = false }) {
1267
+ const dialogId = (0, import_react8.useId)();
1187
1268
  const tokenId = (0, import_react8.useId)();
1188
1269
  const client = (0, import_sanity.useClient)({ apiVersion: "2025-01-01" });
1189
1270
  const [token, setToken] = (0, import_react8.useState)("");
1190
1271
  const [saving, setSaving] = (0, import_react8.useState)(false);
1191
1272
  const [error, setError] = (0, import_react8.useState)(null);
1273
+ const [revoking, setRevoking] = (0, import_react8.useState)(false);
1192
1274
  const save = (0, import_react8.useCallback)(async () => {
1193
1275
  if (!token.trim()) return;
1194
1276
  setSaving(true);
@@ -1206,27 +1288,54 @@ function TokenSetup({ onSaved, onCancel }) {
1206
1288
  setSaving(false);
1207
1289
  }
1208
1290
  }, [client, token, onSaved]);
1291
+ const revoke = (0, import_react8.useCallback)(async () => {
1292
+ setRevoking(true);
1293
+ setError(null);
1294
+ try {
1295
+ await client.delete(TOKEN_DOC_ID);
1296
+ onSaved();
1297
+ } catch (err) {
1298
+ setError(err instanceof Error ? err.message : "Failed to remove token");
1299
+ } finally {
1300
+ setRevoking(false);
1301
+ }
1302
+ }, [client, onSaved]);
1209
1303
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1210
1304
  Dialog,
1211
1305
  {
1212
1306
  header: "Connect Vercel API token",
1213
- id: "token-setup",
1307
+ id: dialogId,
1214
1308
  onClose: onCancel,
1215
1309
  width: 1,
1216
- footer: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Flex, { padding: 3, gap: 2, justify: "flex-end", children: [
1217
- onCancel && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Button, { text: "Cancel", mode: "ghost", onClick: onCancel, style: { cursor: "pointer" } }),
1218
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1310
+ footer: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Flex, { padding: 3, gap: 2, justify: hasToken ? "space-between" : "flex-end", children: [
1311
+ hasToken && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1219
1312
  Button,
1220
1313
  {
1221
- text: "Save and connect",
1222
- tone: "primary",
1223
- icon: CheckmarkCircleIcon,
1224
- loading: saving,
1225
- disabled: !token.trim() || saving,
1226
- onClick: save,
1314
+ text: "Remove token",
1315
+ mode: "ghost",
1316
+ tone: "critical",
1317
+ icon: TrashIcon,
1318
+ loading: revoking,
1319
+ disabled: revoking || saving,
1320
+ onClick: revoke,
1227
1321
  style: { cursor: "pointer" }
1228
1322
  }
1229
- )
1323
+ ),
1324
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Flex, { gap: 2, children: [
1325
+ onCancel && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Button, { text: "Cancel", mode: "ghost", onClick: onCancel, style: { cursor: "pointer" } }),
1326
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1327
+ Button,
1328
+ {
1329
+ text: "Save and connect",
1330
+ tone: "primary",
1331
+ icon: CheckmarkCircleIcon,
1332
+ loading: saving,
1333
+ disabled: !token.trim() || saving,
1334
+ onClick: save,
1335
+ style: { cursor: "pointer" }
1336
+ }
1337
+ )
1338
+ ] })
1230
1339
  ] }),
1231
1340
  children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Stack, { space: 4, padding: 4, children: [
1232
1341
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Stack, { space: 3, children: [
@@ -1267,6 +1376,7 @@ var VERCEL_HOOK_RE2 = /^https:\/\/api\.vercel\.com\/v1\/integrations\/deploy\//;
1267
1376
  function DeployTargetForm({ initial, onSaved, onClose }) {
1268
1377
  const client = (0, import_sanity2.useClient)({ apiVersion: "2025-01-01" });
1269
1378
  const isEdit = Boolean(initial);
1379
+ const dialogId = (0, import_react9.useId)();
1270
1380
  const nameId = (0, import_react9.useId)();
1271
1381
  const urlId = (0, import_react9.useId)();
1272
1382
  const urlErrorId = (0, import_react9.useId)();
@@ -1309,7 +1419,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
1309
1419
  Dialog,
1310
1420
  {
1311
1421
  header: isEdit ? `Edit "${initial?.name}"` : "Add deploy target",
1312
- id: "deploy-target-form",
1422
+ id: dialogId,
1313
1423
  onClose,
1314
1424
  width: 1,
1315
1425
  footer: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(Flex, { padding: 3, gap: 2, justify: "flex-end", children: [
@@ -1408,7 +1518,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
1408
1518
  }
1409
1519
 
1410
1520
  // src/version.ts
1411
- var VERSION = "1.2.0";
1521
+ var VERSION = "1.2.1";
1412
1522
 
1413
1523
  // src/components/DeployTool.tsx
1414
1524
  var import_jsx_runtime12 = require("react/jsx-runtime");
@@ -1613,7 +1723,8 @@ function DeployTool() {
1613
1723
  setShowTokenSetup(false);
1614
1724
  load();
1615
1725
  },
1616
- onCancel: () => setShowTokenSetup(false)
1726
+ onCancel: () => setShowTokenSetup(false),
1727
+ hasToken: Boolean(token)
1617
1728
  }
1618
1729
  ),
1619
1730
  showCreateForm && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
@@ -1760,37 +1871,13 @@ var vercelDeploySchema = (0, import_sanity4.defineType)({
1760
1871
  }
1761
1872
  });
1762
1873
 
1763
- // src/schema/vercelConfig.ts
1764
- var import_sanity5 = require("sanity");
1765
- var vercelConfigSchema = (0, import_sanity5.defineType)({
1766
- name: "vercelDeploy.config",
1767
- title: "Vercel Deploy Configuration",
1768
- type: "document",
1769
- fields: [
1770
- (0, import_sanity5.defineField)({
1771
- name: "accessToken",
1772
- title: "Vercel API Token",
1773
- type: "string",
1774
- description: "Readable by anyone who can read this dataset. Delete this document to revoke the stored token."
1775
- })
1776
- ],
1777
- preview: {
1778
- select: { token: "accessToken" },
1779
- prepare: ({ token }) => ({
1780
- title: "Vercel API Token",
1781
- // Never render the secret itself in a preview.
1782
- subtitle: token ? "Connected" : "Not set"
1783
- })
1784
- }
1785
- });
1786
-
1787
1874
  // src/index.ts
1788
- var vercelDeploy = (0, import_sanity6.definePlugin)((options) => {
1875
+ var vercelDeploy = (0, import_sanity5.definePlugin)((options) => {
1789
1876
  const config = options ?? {};
1790
1877
  return {
1791
1878
  name: "deploy-vercel-from-sanity",
1792
1879
  schema: {
1793
- types: [vercelDeploySchema, vercelConfigSchema]
1880
+ types: [vercelDeploySchema]
1794
1881
  },
1795
1882
  tools: [
1796
1883
  {
@@ -1804,7 +1891,6 @@ var vercelDeploy = (0, import_sanity6.definePlugin)((options) => {
1804
1891
  });
1805
1892
  // Annotate the CommonJS export names for ESM import in node:
1806
1893
  0 && (module.exports = {
1807
- vercelConfigSchema,
1808
1894
  vercelDeploy,
1809
1895
  vercelDeploySchema
1810
1896
  });