@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
@@ -1,5 +1,11 @@
1
1
  # @se-studio/skills
2
2
 
3
+ ## 1.0.31
4
+
5
+ ### Patch Changes
6
+
7
+ - Version bump: patch for changed packages
8
+
3
9
  ## 1.0.30
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@se-studio/skills",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "description": "SE Studio agent skills for marketing site development with Contentful CMS",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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. Sets REVALIDATION_SECRET in Vercel (production + preview)
71
- * 4. Updates .env.local with the new secret
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 = `${siteUrl}/api/revalidate?secret=${secret}`;
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 webhook with the same name (idempotent)
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 topics = [
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
- { name: WEBHOOK_NAME, url: webhookUrl, topics, active: true, headers: [] },
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(`Webhook created: ${webhook.sys.id}`);
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(` Webhook: ${WEBHOOK_NAME} (${webhook.sys.id})`);
213
- console.log(` URL: ${webhookUrl}`);
214
- console.log(` Secret: ${secret.slice(0, 8)}…`);
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?secret=<REVALIDATION_SECRET>"
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 URL doesn't match the deployed env var (check Vercel dashboard → Settings → Environment Variables).
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