@se-studio/skills 1.0.30 → 1.0.31
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/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -53,6 +53,8 @@ Store this as `SITE_URL` for use in subsequent steps.
|
|
|
53
53
|
|
|
54
54
|
Write this file to the project. It handles secret generation, webhook creation, and Vercel env var updates in one run.
|
|
55
55
|
|
|
56
|
+
The secret is passed as a **request header** (`REVALIDATION_SECRET`), not a query parameter. Optionally, a second preview webhook is created when `PREVIEW_SITE_URL` and `VERCEL_PROTECTION_BYPASS_TOKEN` are both set in `.env.local`.
|
|
57
|
+
|
|
56
58
|
```typescript
|
|
57
59
|
/**
|
|
58
60
|
* Set up Contentful revalidation webhook and Vercel env vars.
|
|
@@ -64,11 +66,17 @@ Write this file to the project. It handles secret generation, webhook creation,
|
|
|
64
66
|
* - CONTENTFUL_SPACE_ID and CONTENTFUL_MANAGEMENT_TOKEN set in .env.local
|
|
65
67
|
* - Vercel CLI authenticated (vercel whoami)
|
|
66
68
|
*
|
|
69
|
+
* Optional env vars in .env.local:
|
|
70
|
+
* PREVIEW_SITE_URL=https://your-preview.vercel.app
|
|
71
|
+
* VERCEL_PROTECTION_BYPASS_TOKEN=<token from Vercel → Settings → Deployment Protection>
|
|
72
|
+
* If both are set, a "Vercel Revalidation (Preview)" webhook is created for draft events.
|
|
73
|
+
*
|
|
67
74
|
* What it does:
|
|
68
75
|
* 1. Generates REVALIDATION_SECRET if not already set
|
|
69
|
-
* 2. Creates (or replaces) the Contentful revalidation webhook
|
|
70
|
-
* 3.
|
|
71
|
-
* 4.
|
|
76
|
+
* 2. Creates (or replaces) the Contentful production revalidation webhook (header auth)
|
|
77
|
+
* 3. Optionally creates a preview webhook with x-vercel-protection-bypass header
|
|
78
|
+
* 4. Sets REVALIDATION_SECRET in Vercel (production + preview)
|
|
79
|
+
* 5. Updates .env.local with the new secret
|
|
72
80
|
*/
|
|
73
81
|
|
|
74
82
|
import { createClient } from 'contentful-management';
|
|
@@ -87,6 +95,8 @@ if (!siteUrl || !siteUrl.startsWith('https://')) {
|
|
|
87
95
|
process.exit(1);
|
|
88
96
|
}
|
|
89
97
|
|
|
98
|
+
const normalizedSiteUrl = siteUrl.replace(/\/$/, '');
|
|
99
|
+
|
|
90
100
|
// ---------------------------------------------------------------------------
|
|
91
101
|
// Load .env.local
|
|
92
102
|
// ---------------------------------------------------------------------------
|
|
@@ -100,6 +110,10 @@ function getEnv(key: string): string {
|
|
|
100
110
|
return value;
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
function tryGetEnv(key: string): string | undefined {
|
|
114
|
+
return envContent.match(new RegExp(`^${key}=(.+)`, 'm'))?.[1]?.trim() ?? process.env[key];
|
|
115
|
+
}
|
|
116
|
+
|
|
103
117
|
function setEnvLocal(key: string, value: string): void {
|
|
104
118
|
if (new RegExp(`^${key}=`, 'm').test(envContent)) {
|
|
105
119
|
envContent = envContent.replace(new RegExp(`^${key}=.*`, 'm'), `${key}=${value}`);
|
|
@@ -136,10 +150,23 @@ function vercelSetEnv(name: string, value: string, env: string): void {
|
|
|
136
150
|
// ---------------------------------------------------------------------------
|
|
137
151
|
|
|
138
152
|
const WEBHOOK_NAME = 'Vercel Revalidation';
|
|
153
|
+
const ASSET_WEBHOOK_NAME = 'Vercel Revalidation (Assets)';
|
|
154
|
+
const PREVIEW_WEBHOOK_NAME = 'Vercel Revalidation (Preview)';
|
|
155
|
+
|
|
156
|
+
const ENTRY_CONTENT_TYPES = [
|
|
157
|
+
'article', 'articleType', 'banner', 'customType', 'location',
|
|
158
|
+
'navigation', 'page', 'pageVariant', 'person', 'tag', 'template',
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const contentTypeFilter = {
|
|
162
|
+
in: [{ doc: 'sys.contentType.sys.id' }, ENTRY_CONTENT_TYPES],
|
|
163
|
+
};
|
|
139
164
|
|
|
140
165
|
async function main(): Promise<void> {
|
|
141
166
|
const managementToken = getEnv('CONTENTFUL_MANAGEMENT_TOKEN');
|
|
142
167
|
const spaceId = getEnv('CONTENTFUL_SPACE_ID');
|
|
168
|
+
const previewSiteUrl = tryGetEnv('PREVIEW_SITE_URL')?.replace(/\/$/, '');
|
|
169
|
+
const bypassToken = tryGetEnv('VERCEL_PROTECTION_BYPASS_TOKEN');
|
|
143
170
|
|
|
144
171
|
// Generate secret if not already set
|
|
145
172
|
let secret = envContent.match(/^REVALIDATION_SECRET=(.+)/m)?.[1]?.trim();
|
|
@@ -150,29 +177,33 @@ async function main(): Promise<void> {
|
|
|
150
177
|
console.log('Using existing REVALIDATION_SECRET from .env.local');
|
|
151
178
|
}
|
|
152
179
|
|
|
153
|
-
const webhookUrl = `${
|
|
180
|
+
const webhookUrl = `${normalizedSiteUrl}/api/revalidate`;
|
|
154
181
|
|
|
155
182
|
// -------------------------------------------------------------------------
|
|
156
|
-
// Create Contentful webhook
|
|
183
|
+
// Create Contentful production webhook
|
|
157
184
|
// -------------------------------------------------------------------------
|
|
158
185
|
|
|
159
186
|
console.log('Connecting to Contentful…');
|
|
160
187
|
const client = createClient({ accessToken: managementToken });
|
|
161
188
|
|
|
162
|
-
// Delete existing
|
|
189
|
+
// Delete existing webhooks with the same names (idempotent)
|
|
163
190
|
const existing = await client.webhook.getMany({ spaceId, query: { limit: 100 } });
|
|
191
|
+
|
|
164
192
|
const duplicate = existing.items.find((w) => w.name === WEBHOOK_NAME);
|
|
165
193
|
if (duplicate) {
|
|
166
194
|
console.log(`Removing existing webhook: ${duplicate.sys.id}`);
|
|
167
195
|
await client.webhook.delete({ spaceId, webhookDefinitionId: duplicate.sys.id });
|
|
168
196
|
}
|
|
169
197
|
|
|
170
|
-
const
|
|
198
|
+
const entryTopics = [
|
|
171
199
|
'Entry.publish',
|
|
172
200
|
'Entry.unpublish',
|
|
173
201
|
'Entry.delete',
|
|
174
202
|
'Entry.archive',
|
|
175
203
|
'Entry.unarchive',
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const assetTopics = [
|
|
176
207
|
'Asset.publish',
|
|
177
208
|
'Asset.unpublish',
|
|
178
209
|
'Asset.delete',
|
|
@@ -180,12 +211,83 @@ async function main(): Promise<void> {
|
|
|
180
211
|
'Asset.unarchive',
|
|
181
212
|
];
|
|
182
213
|
|
|
183
|
-
console.log(`Creating webhook → ${webhookUrl}`);
|
|
214
|
+
console.log(`Creating production entry webhook → ${webhookUrl}`);
|
|
184
215
|
const webhook = await client.webhook.create(
|
|
185
216
|
{ spaceId },
|
|
186
|
-
{
|
|
217
|
+
{
|
|
218
|
+
name: WEBHOOK_NAME,
|
|
219
|
+
url: webhookUrl,
|
|
220
|
+
topics: entryTopics,
|
|
221
|
+
filters: [contentTypeFilter],
|
|
222
|
+
active: true,
|
|
223
|
+
headers: [{ key: 'REVALIDATION_SECRET', value: secret }],
|
|
224
|
+
},
|
|
187
225
|
);
|
|
188
|
-
console.log(`
|
|
226
|
+
console.log(`Production entry webhook created: ${webhook.sys.id}`);
|
|
227
|
+
|
|
228
|
+
const duplicateAsset = existing.items.find((w) => w.name === ASSET_WEBHOOK_NAME);
|
|
229
|
+
if (duplicateAsset) {
|
|
230
|
+
console.log(`Removing existing asset webhook: ${duplicateAsset.sys.id}`);
|
|
231
|
+
await client.webhook.delete({ spaceId, webhookDefinitionId: duplicateAsset.sys.id });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(`Creating production asset webhook → ${webhookUrl}`);
|
|
235
|
+
const assetWebhook = await client.webhook.create(
|
|
236
|
+
{ spaceId },
|
|
237
|
+
{
|
|
238
|
+
name: ASSET_WEBHOOK_NAME,
|
|
239
|
+
url: webhookUrl,
|
|
240
|
+
topics: assetTopics,
|
|
241
|
+
active: true,
|
|
242
|
+
headers: [{ key: 'REVALIDATION_SECRET', value: secret }],
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
console.log(`Production asset webhook created: ${assetWebhook.sys.id}`);
|
|
246
|
+
|
|
247
|
+
// -------------------------------------------------------------------------
|
|
248
|
+
// Create optional preview webhook
|
|
249
|
+
// -------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
if (previewSiteUrl && bypassToken) {
|
|
252
|
+
const previewWebhookUrl = `${previewSiteUrl}/api/revalidate`;
|
|
253
|
+
|
|
254
|
+
const existingPreview = existing.items.find((w) => w.name === PREVIEW_WEBHOOK_NAME);
|
|
255
|
+
if (existingPreview) {
|
|
256
|
+
console.log(`Removing existing preview webhook: ${existingPreview.sys.id}`);
|
|
257
|
+
await client.webhook.delete({ spaceId, webhookDefinitionId: existingPreview.sys.id });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const draftTopics = [
|
|
261
|
+
'Entry.save',
|
|
262
|
+
'Entry.autosave',
|
|
263
|
+
'Entry.publish',
|
|
264
|
+
'Entry.unpublish',
|
|
265
|
+
'Entry.delete',
|
|
266
|
+
'Asset.publish',
|
|
267
|
+
'Asset.unpublish',
|
|
268
|
+
'Asset.delete',
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
console.log(`Creating preview webhook → ${previewWebhookUrl}`);
|
|
272
|
+
const previewWebhook = await client.webhook.create(
|
|
273
|
+
{ spaceId },
|
|
274
|
+
{
|
|
275
|
+
name: PREVIEW_WEBHOOK_NAME,
|
|
276
|
+
url: previewWebhookUrl,
|
|
277
|
+
topics: draftTopics,
|
|
278
|
+
active: true,
|
|
279
|
+
headers: [
|
|
280
|
+
{ key: 'REVALIDATION_SECRET', value: secret },
|
|
281
|
+
{ key: 'x-vercel-protection-bypass', value: bypassToken },
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
console.log(`Preview webhook created: ${previewWebhook.sys.id}`);
|
|
286
|
+
} else if (previewSiteUrl || bypassToken) {
|
|
287
|
+
console.warn(
|
|
288
|
+
'Skipping preview webhook: both PREVIEW_SITE_URL and VERCEL_PROTECTION_BYPASS_TOKEN must be set in .env.local.',
|
|
289
|
+
);
|
|
290
|
+
}
|
|
189
291
|
|
|
190
292
|
// -------------------------------------------------------------------------
|
|
191
293
|
// Update Vercel env vars
|
|
@@ -209,9 +311,10 @@ async function main(): Promise<void> {
|
|
|
209
311
|
// -------------------------------------------------------------------------
|
|
210
312
|
|
|
211
313
|
console.log('\nDone!');
|
|
212
|
-
console.log(`
|
|
213
|
-
console.log(`
|
|
214
|
-
console.log(`
|
|
314
|
+
console.log(` Entry webhook: ${WEBHOOK_NAME} (${webhook.sys.id})`);
|
|
315
|
+
console.log(` Asset webhook: ${ASSET_WEBHOOK_NAME} (${assetWebhook.sys.id})`);
|
|
316
|
+
console.log(` URL: ${webhookUrl}`);
|
|
317
|
+
console.log(` Secret: ${secret.slice(0, 8)}…`);
|
|
215
318
|
console.log('\nNext: set up Content Preview in Contentful (see skill Step 5)');
|
|
216
319
|
console.log('Then redeploy to pick up the new secret:');
|
|
217
320
|
console.log(' vercel deploy --prod');
|
|
@@ -273,10 +376,10 @@ Or push to the production branch if the project uses git-based deploys.
|
|
|
273
376
|
#### Test the revalidation webhook
|
|
274
377
|
|
|
275
378
|
```bash
|
|
276
|
-
curl -X POST "https://<SITE_URL>/api/revalidate
|
|
379
|
+
curl -X POST "https://<SITE_URL>/api/revalidate" -H "REVALIDATION_SECRET: <REVALIDATION_SECRET>"
|
|
277
380
|
```
|
|
278
381
|
|
|
279
|
-
Expected: `200 OK`. A `401` means the secret in the
|
|
382
|
+
Expected: `200 OK`. A `401` means the secret in the header doesn't match the deployed env var (check Vercel dashboard → Settings → Environment Variables).
|
|
280
383
|
|
|
281
384
|
#### Confirm webhook delivery in Contentful
|
|
282
385
|
|