@liiift-studio/deploy-vercel-from-sanity 0.1.4 → 0.1.7

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/src/lib/api.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  // Vercel REST API helpers — all calls require a bearer token
2
- import type { VercelDeployment } from '../types'
2
+ import type { VercelDeployment, DeploymentEvent } from '../types'
3
3
 
4
4
  const BASE = 'https://api.vercel.com'
5
5
 
6
+ /** Only allow genuine Vercel deploy hook URLs through triggerDeploy */
7
+ const VERCEL_HOOK_RE = /^https:\/\/api\.vercel\.com\/v1\/integrations\/deploy\//
8
+
6
9
  async function vercelFetch<T>(path: string, token: string, init?: RequestInit): Promise<T> {
7
10
  const res = await fetch(`${BASE}${path}`, {
8
11
  ...init,
@@ -52,8 +55,36 @@ export async function cancelDeployment(opts: {
52
55
  })
53
56
  }
54
57
 
55
- /** Trigger a deploy by POSTing to the hook URL — no auth needed */
58
+ /**
59
+ * Trigger a deploy by POSTing to the hook URL.
60
+ * Validates the URL is a genuine Vercel hook before calling to prevent
61
+ * SSRF if a document is tampered with outside the Studio schema.
62
+ */
56
63
  export async function triggerDeploy(hookUrl: string): Promise<void> {
64
+ if (!VERCEL_HOOK_RE.test(hookUrl)) {
65
+ throw new Error('Invalid deploy hook URL — must be a Vercel hook (api.vercel.com/v1/integrations/deploy/…)')
66
+ }
57
67
  const res = await fetch(hookUrl, { method: 'POST' })
58
68
  if (!res.ok) throw new Error(`Deploy hook returned ${res.status}`)
59
69
  }
70
+
71
+ /**
72
+ * Fetch build events for a deployment.
73
+ * Returns up to 100 events in reverse chronological order,
74
+ * filtered to lines with actual text content.
75
+ */
76
+ export async function getDeploymentEvents(opts: {
77
+ deploymentId: string
78
+ token: string
79
+ teamId?: string
80
+ }): Promise<DeploymentEvent[]> {
81
+ const params = new URLSearchParams({ limit: '100', direction: 'backward' })
82
+ if (opts.teamId) params.set('teamId', opts.teamId)
83
+ // API returns either a plain array or a wrapped object depending on version
84
+ const raw = await vercelFetch<DeploymentEvent[] | { events?: DeploymentEvent[] }>(
85
+ `/v2/deployments/${opts.deploymentId}/events?${params}`,
86
+ opts.token,
87
+ )
88
+ const events: DeploymentEvent[] = Array.isArray(raw) ? raw : (raw.events ?? [])
89
+ return events.filter(e => e.text?.trim())
90
+ }
@@ -74,13 +74,30 @@ export function stateLabel(state: VercelDeployState | undefined): {
74
74
  tone: 'positive' | 'caution' | 'critical' | 'default'
75
75
  } {
76
76
  switch (state) {
77
- case 'READY': return { label: 'Ready', tone: 'positive' }
78
- case 'BUILDING': return { label: 'Building', tone: 'caution' }
79
- case 'QUEUED': return { label: 'Queued', tone: 'caution' }
80
- case 'INITIALIZING':return { label: 'Initializing', tone: 'caution' }
81
- case 'ERROR': return { label: 'Error', tone: 'critical' }
82
- case 'CANCELED': return { label: 'Canceled', tone: 'default' }
83
- case 'LOADING': return { label: 'Loading…', tone: 'default' }
84
- default: return { label: 'Unknown', tone: 'default' }
77
+ case 'READY': return { label: 'Ready', tone: 'positive' }
78
+ case 'BUILDING': return { label: 'Building', tone: 'caution' }
79
+ case 'QUEUED': return { label: 'Queued', tone: 'caution' }
80
+ case 'INITIALIZING': return { label: 'Initializing', tone: 'caution' }
81
+ case 'ERROR': return { label: 'Error', tone: 'critical' }
82
+ case 'CANCELED': return { label: 'Canceled', tone: 'default' }
83
+ case 'LOADING': return { label: 'Loading…', tone: 'default' }
84
+ default: return { label: 'Unknown', tone: 'default' }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Extracts the Vercel project dashboard URL from a deployment's inspectorUrl.
90
+ * inspectorUrl format: https://vercel.com/{team}/{project}/{deploymentId}
91
+ * Returns https://vercel.com/{team}/{project} or null if unparseable.
92
+ */
93
+ export function projectHref(inspectorUrl: string | undefined): string | null {
94
+ if (!inspectorUrl) return null
95
+ try {
96
+ const { origin, pathname } = new URL(inspectorUrl)
97
+ const parts = pathname.split('/').filter(Boolean)
98
+ if (parts.length < 2) return null
99
+ return `${origin}/${parts[0]}/${parts[1]}`
100
+ } catch {
101
+ return null
85
102
  }
86
103
  }
package/src/types.ts CHANGED
@@ -45,9 +45,17 @@ export interface VercelDeployment {
45
45
  }
46
46
  }
47
47
 
48
- /** Vercel secrets document stored at _id: 'secrets.vercelDeploy' */
49
- export interface VercelSecrets {
50
- _id: 'secrets.vercelDeploy'
48
+ /** A single build event returned by GET /v2/deployments/{id}/events */
49
+ export interface DeploymentEvent {
50
+ type: 'command' | 'stdout' | 'stderr' | 'exit' | 'deployment-state'
51
+ text?: string
52
+ created: number
53
+ payload?: Record<string, unknown>
54
+ }
55
+
56
+ /** Vercel config document stored at _id: 'config.vercelDeploy' — readable by all authenticated users */
57
+ export interface VercelConfig {
58
+ _id: 'config.vercelDeploy'
51
59
  _type: 'vercelDeploy.config'
52
60
  accessToken: string
53
61
  }