shopify-starter-kit 1.0.0

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.
Files changed (28) hide show
  1. package/.agent/skills/shopify-apps/SKILL.md +47 -0
  2. package/.agent/skills/shopify-automation/SKILL.md +172 -0
  3. package/.agent/skills/shopify-development/README.md +60 -0
  4. package/.agent/skills/shopify-development/SKILL.md +368 -0
  5. package/.agent/skills/shopify-development/references/app-development.md +578 -0
  6. package/.agent/skills/shopify-development/references/extensions.md +555 -0
  7. package/.agent/skills/shopify-development/references/themes.md +498 -0
  8. package/.agent/skills/shopify-development/scripts/requirements.txt +19 -0
  9. package/.agent/skills/shopify-development/scripts/shopify_graphql.py +428 -0
  10. package/.agent/skills/shopify-development/scripts/shopify_init.py +441 -0
  11. package/.agent/skills/shopify-development/scripts/tests/test_shopify_init.py +379 -0
  12. package/bin/cli.js +3 -0
  13. package/package.json +32 -0
  14. package/src/index.js +116 -0
  15. package/templates/.agent/skills/shopify-apps/SKILL.md +47 -0
  16. package/templates/.agent/skills/shopify-automation/SKILL.md +172 -0
  17. package/templates/.agent/skills/shopify-development/README.md +60 -0
  18. package/templates/.agent/skills/shopify-development/SKILL.md +368 -0
  19. package/templates/.agent/skills/shopify-development/references/app-development.md +578 -0
  20. package/templates/.agent/skills/shopify-development/references/extensions.md +555 -0
  21. package/templates/.agent/skills/shopify-development/references/themes.md +498 -0
  22. package/templates/.agent/skills/shopify-development/scripts/requirements.txt +19 -0
  23. package/templates/.agent/skills/shopify-development/scripts/shopify_graphql.py +428 -0
  24. package/templates/.agent/skills/shopify-development/scripts/shopify_init.py +441 -0
  25. package/templates/.agent/skills/shopify-development/scripts/tests/test_shopify_init.py +379 -0
  26. package/templates/.devcontainer/devcontainer.json +27 -0
  27. package/templates/tests/playwright.config.ts +26 -0
  28. package/templates/tests/vitest.config.ts +9 -0
@@ -0,0 +1,379 @@
1
+ """
2
+ Tests for shopify_init.py
3
+
4
+ Run with: pytest test_shopify_init.py -v --cov=shopify_init --cov-report=term-missing
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ import pytest
11
+ import subprocess
12
+ from pathlib import Path
13
+ from unittest.mock import Mock, patch, mock_open, MagicMock
14
+
15
+ sys.path.insert(0, str(Path(__file__).parent.parent))
16
+
17
+ from shopify_init import EnvLoader, EnvConfig, ShopifyInitializer
18
+
19
+
20
+ class TestEnvLoader:
21
+ """Test EnvLoader class."""
22
+
23
+ def test_load_env_file_success(self, tmp_path):
24
+ """Test loading valid .env file."""
25
+ env_file = tmp_path / ".env"
26
+ env_file.write_text("""
27
+ SHOPIFY_API_KEY=test_key
28
+ SHOPIFY_API_SECRET=test_secret
29
+ SHOP_DOMAIN=test.myshopify.com
30
+ # Comment line
31
+ SCOPES=read_products,write_products
32
+ """)
33
+
34
+ result = EnvLoader.load_env_file(env_file)
35
+
36
+ assert result['SHOPIFY_API_KEY'] == 'test_key'
37
+ assert result['SHOPIFY_API_SECRET'] == 'test_secret'
38
+ assert result['SHOP_DOMAIN'] == 'test.myshopify.com'
39
+ assert result['SCOPES'] == 'read_products,write_products'
40
+
41
+ def test_load_env_file_with_quotes(self, tmp_path):
42
+ """Test loading .env file with quoted values."""
43
+ env_file = tmp_path / ".env"
44
+ env_file.write_text("""
45
+ SHOPIFY_API_KEY="test_key"
46
+ SHOPIFY_API_SECRET='test_secret'
47
+ """)
48
+
49
+ result = EnvLoader.load_env_file(env_file)
50
+
51
+ assert result['SHOPIFY_API_KEY'] == 'test_key'
52
+ assert result['SHOPIFY_API_SECRET'] == 'test_secret'
53
+
54
+ def test_load_env_file_nonexistent(self, tmp_path):
55
+ """Test loading non-existent .env file."""
56
+ result = EnvLoader.load_env_file(tmp_path / "nonexistent.env")
57
+ assert result == {}
58
+
59
+ def test_load_env_file_invalid_format(self, tmp_path):
60
+ """Test loading .env file with invalid lines."""
61
+ env_file = tmp_path / ".env"
62
+ env_file.write_text("""
63
+ VALID_KEY=value
64
+ INVALID_LINE_NO_EQUALS
65
+ ANOTHER_VALID=test
66
+ """)
67
+
68
+ result = EnvLoader.load_env_file(env_file)
69
+
70
+ assert result['VALID_KEY'] == 'value'
71
+ assert result['ANOTHER_VALID'] == 'test'
72
+ assert 'INVALID_LINE_NO_EQUALS' not in result
73
+
74
+ def test_get_env_paths(self, tmp_path):
75
+ """Test getting .env file paths from universal directory structure."""
76
+ # Create directory structure (works with .agent, .claude, .gemini, .cursor)
77
+ agent_dir = tmp_path / ".agent"
78
+ skills_dir = agent_dir / "skills"
79
+ skill_dir = skills_dir / "shopify"
80
+
81
+ skill_dir.mkdir(parents=True)
82
+
83
+ # Create .env files at each level
84
+ (skill_dir / ".env").write_text("SKILL=1")
85
+ (skills_dir / ".env").write_text("SKILLS=1")
86
+ (agent_dir / ".env").write_text("AGENT=1")
87
+
88
+ paths = EnvLoader.get_env_paths(skill_dir)
89
+
90
+ assert len(paths) == 3
91
+ assert skill_dir / ".env" in paths
92
+ assert skills_dir / ".env" in paths
93
+ assert agent_dir / ".env" in paths
94
+
95
+ def test_load_config_priority(self, tmp_path, monkeypatch):
96
+ """Test configuration loading priority across different AI tool directories."""
97
+ skill_dir = tmp_path / "skill"
98
+ skills_dir = tmp_path
99
+ agent_dir = tmp_path.parent # Could be .agent, .claude, .gemini, .cursor
100
+
101
+ skill_dir.mkdir(parents=True)
102
+
103
+ (skill_dir / ".env").write_text("SHOPIFY_API_KEY=skill_key")
104
+ (skills_dir / ".env").write_text("SHOPIFY_API_KEY=skills_key\nSHOP_DOMAIN=skills.myshopify.com")
105
+
106
+ monkeypatch.setenv("SHOPIFY_API_KEY", "process_key")
107
+
108
+ config = EnvLoader.load_config(skill_dir)
109
+
110
+ assert config.shopify_api_key == "process_key"
111
+ # Shop domain from skills/.env
112
+ assert config.shop_domain == "skills.myshopify.com"
113
+
114
+ def test_load_config_no_files(self, tmp_path):
115
+ """Test configuration loading with no .env files."""
116
+ config = EnvLoader.load_config(tmp_path)
117
+
118
+ assert config.shopify_api_key is None
119
+ assert config.shopify_api_secret is None
120
+ assert config.shop_domain is None
121
+ assert config.scopes is None
122
+
123
+
124
+ class TestShopifyInitializer:
125
+ """Test ShopifyInitializer class."""
126
+
127
+ @pytest.fixture
128
+ def config(self):
129
+ """Create test config."""
130
+ return EnvConfig(
131
+ shopify_api_key="test_key",
132
+ shopify_api_secret="test_secret",
133
+ shop_domain="test.myshopify.com",
134
+ scopes="read_products,write_products"
135
+ )
136
+
137
+ @pytest.fixture
138
+ def initializer(self, config):
139
+ """Create initializer instance."""
140
+ return ShopifyInitializer(config)
141
+
142
+ def test_prompt_with_default(self, initializer):
143
+ """Test prompt with default value."""
144
+ with patch('builtins.input', return_value=''):
145
+ result = initializer.prompt("Test", "default_value")
146
+ assert result == "default_value"
147
+
148
+ def test_prompt_with_input(self, initializer):
149
+ """Test prompt with user input."""
150
+ with patch('builtins.input', return_value='user_input'):
151
+ result = initializer.prompt("Test", "default_value")
152
+ assert result == "user_input"
153
+
154
+ def test_select_option_valid(self, initializer):
155
+ """Test select option with valid choice."""
156
+ options = ['app', 'extension', 'theme']
157
+ with patch('builtins.input', return_value='2'):
158
+ result = initializer.select_option("Choose", options)
159
+ assert result == 'extension'
160
+
161
+ def test_select_option_invalid_then_valid(self, initializer):
162
+ """Test select option with invalid then valid choice."""
163
+ options = ['app', 'extension']
164
+ with patch('builtins.input', side_effect=['5', 'invalid', '1']):
165
+ result = initializer.select_option("Choose", options)
166
+ assert result == 'app'
167
+
168
+ def test_check_cli_installed_success(self, initializer):
169
+ """Test CLI installed check - success."""
170
+ mock_result = Mock()
171
+ mock_result.returncode = 0
172
+
173
+ with patch('subprocess.run', return_value=mock_result):
174
+ assert initializer.check_cli_installed() is True
175
+
176
+ def test_check_cli_installed_failure(self, initializer):
177
+ """Test CLI installed check - failure."""
178
+ with patch('subprocess.run', side_effect=FileNotFoundError):
179
+ assert initializer.check_cli_installed() is False
180
+
181
+ def test_create_app_config(self, initializer, tmp_path):
182
+ """Test creating app configuration file."""
183
+ initializer.create_app_config(tmp_path, "test-app", "read_products")
184
+
185
+ config_file = tmp_path / "shopify.app.toml"
186
+ assert config_file.exists()
187
+
188
+ content = config_file.read_text()
189
+ assert 'name = "test-app"' in content
190
+ assert 'scopes = "read_products"' in content
191
+ assert 'client_id = "test_key"' in content
192
+
193
+ def test_create_extension_config(self, initializer, tmp_path):
194
+ """Test creating extension configuration file."""
195
+ initializer.create_extension_config(tmp_path, "test-ext", "checkout")
196
+
197
+ config_file = tmp_path / "shopify.extension.toml"
198
+ assert config_file.exists()
199
+
200
+ content = config_file.read_text()
201
+ assert 'name = "test-ext"' in content
202
+ assert 'purchase.checkout.block.render' in content
203
+
204
+ def test_create_extension_config_admin_action(self, initializer, tmp_path):
205
+ """Test creating admin action extension config."""
206
+ initializer.create_extension_config(tmp_path, "admin-ext", "admin_action")
207
+
208
+ config_file = tmp_path / "shopify.extension.toml"
209
+ content = config_file.read_text()
210
+ assert 'admin.product-details.action.render' in content
211
+
212
+ def test_create_readme(self, initializer, tmp_path):
213
+ """Test creating README file."""
214
+ initializer.create_readme(tmp_path, "app", "Test App")
215
+
216
+ readme_file = tmp_path / "README.md"
217
+ assert readme_file.exists()
218
+
219
+ content = readme_file.read_text()
220
+ assert '# Test App' in content
221
+ assert 'shopify app dev' in content
222
+
223
+ @patch('builtins.input')
224
+ @patch('builtins.print')
225
+ def test_init_app(self, mock_print, mock_input, initializer, tmp_path, monkeypatch):
226
+ """Test app initialization."""
227
+ monkeypatch.chdir(tmp_path)
228
+
229
+ # Mock user inputs
230
+ mock_input.side_effect = ['my-app', 'read_products,write_products']
231
+
232
+ initializer.init_app()
233
+
234
+ # Check directory created
235
+ app_dir = tmp_path / "my-app"
236
+ assert app_dir.exists()
237
+
238
+ # Check files created
239
+ assert (app_dir / "shopify.app.toml").exists()
240
+ assert (app_dir / "README.md").exists()
241
+ assert (app_dir / "package.json").exists()
242
+
243
+ # Check package.json content
244
+ package_json = json.loads((app_dir / "package.json").read_text())
245
+ assert package_json['name'] == 'my-app'
246
+ assert 'dev' in package_json['scripts']
247
+
248
+ @patch('builtins.input')
249
+ @patch('builtins.print')
250
+ def test_init_extension(self, mock_print, mock_input, initializer, tmp_path, monkeypatch):
251
+ """Test extension initialization."""
252
+ monkeypatch.chdir(tmp_path)
253
+
254
+ # Mock user inputs: type selection (1 = checkout), name
255
+ mock_input.side_effect = ['1', 'my-extension']
256
+
257
+ initializer.init_extension()
258
+
259
+ # Check directory and files created
260
+ ext_dir = tmp_path / "my-extension"
261
+ assert ext_dir.exists()
262
+ assert (ext_dir / "shopify.extension.toml").exists()
263
+ assert (ext_dir / "README.md").exists()
264
+
265
+ @patch('builtins.input')
266
+ @patch('builtins.print')
267
+ def test_init_theme(self, mock_print, mock_input, initializer):
268
+ """Test theme initialization."""
269
+ mock_input.return_value = 'my-theme'
270
+
271
+ initializer.init_theme()
272
+
273
+ assert mock_print.called
274
+
275
+ @patch('builtins.print')
276
+ def test_run_no_cli(self, mock_print, initializer):
277
+ """Test run when CLI not installed."""
278
+ with patch.object(initializer, 'check_cli_installed', return_value=False):
279
+ with pytest.raises(SystemExit) as exc_info:
280
+ initializer.run()
281
+ assert exc_info.value.code == 1
282
+
283
+ @patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True)
284
+ @patch.object(ShopifyInitializer, 'init_app')
285
+ @patch('builtins.input')
286
+ @patch('builtins.print')
287
+ def test_run_app_selected(self, mock_print, mock_input, mock_init_app, mock_cli_check, initializer):
288
+ """Test run with app selection."""
289
+ mock_input.return_value = '1' # Select app
290
+
291
+ initializer.run()
292
+
293
+ mock_init_app.assert_called_once()
294
+
295
+ @patch.object(ShopifyInitializer, 'check_cli_installed', return_value=True)
296
+ @patch.object(ShopifyInitializer, 'init_extension')
297
+ @patch('builtins.input')
298
+ @patch('builtins.print')
299
+ def test_run_extension_selected(self, mock_print, mock_input, mock_init_ext, mock_cli_check, initializer):
300
+ """Test run with extension selection."""
301
+ mock_input.return_value = '2' # Select extension
302
+
303
+ initializer.run()
304
+
305
+ mock_init_ext.assert_called_once()
306
+
307
+
308
+ class TestMain:
309
+ """Test main function."""
310
+
311
+ @patch('shopify_init.ShopifyInitializer')
312
+ @patch('shopify_init.EnvLoader')
313
+ def test_main_success(self, mock_loader, mock_initializer):
314
+ """Test main function success path."""
315
+ from shopify_init import main
316
+
317
+ mock_config = Mock()
318
+ mock_loader.load_config.return_value = mock_config
319
+
320
+ mock_init_instance = Mock()
321
+ mock_initializer.return_value = mock_init_instance
322
+
323
+ with patch('builtins.print'):
324
+ main()
325
+
326
+ mock_init_instance.run.assert_called_once()
327
+
328
+ @patch('shopify_init.ShopifyInitializer')
329
+ @patch('sys.exit')
330
+ def test_main_keyboard_interrupt(self, mock_exit, mock_initializer):
331
+ """Test main function with keyboard interrupt."""
332
+ from shopify_init import main
333
+
334
+ mock_initializer.return_value.run.side_effect = KeyboardInterrupt
335
+
336
+ with patch('builtins.print'):
337
+ main()
338
+
339
+ mock_exit.assert_called_with(0)
340
+
341
+ @patch('shopify_init.ShopifyInitializer')
342
+ @patch('sys.exit')
343
+ def test_main_exception(self, mock_exit, mock_initializer):
344
+ """Test main function with exception."""
345
+ from shopify_init import main
346
+
347
+ mock_initializer.return_value.run.side_effect = Exception("Test error")
348
+
349
+ with patch('builtins.print'):
350
+ main()
351
+
352
+ mock_exit.assert_called_with(1)
353
+
354
+
355
+ class TestEnvConfig:
356
+ """Test EnvConfig dataclass."""
357
+
358
+ def test_env_config_defaults(self):
359
+ """Test EnvConfig default values."""
360
+ config = EnvConfig()
361
+
362
+ assert config.shopify_api_key is None
363
+ assert config.shopify_api_secret is None
364
+ assert config.shop_domain is None
365
+ assert config.scopes is None
366
+
367
+ def test_env_config_with_values(self):
368
+ """Test EnvConfig with values."""
369
+ config = EnvConfig(
370
+ shopify_api_key="key",
371
+ shopify_api_secret="secret",
372
+ shop_domain="test.myshopify.com",
373
+ scopes="read_products"
374
+ )
375
+
376
+ assert config.shopify_api_key == "key"
377
+ assert config.shopify_api_secret == "secret"
378
+ assert config.shop_domain == "test.myshopify.com"
379
+ assert config.scopes == "read_products"
package/bin/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('../src/index.js');
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "shopify-starter-kit",
3
+ "version": "1.0.0",
4
+ "description": "Next-Gen Shopify App CLI with pre-configured agent skills, devcontainers, and testing pipelines.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "shopify-starter-kit": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node ./bin/cli.js"
11
+ },
12
+ "dependencies": {
13
+ "chalk": "^4.1.2",
14
+ "commander": "^13.1.0",
15
+ "execa": "^5.1.1",
16
+ "fs-extra": "^11.3.0",
17
+ "prompts": "^2.4.2"
18
+ },
19
+ "keywords": [
20
+ "shopify",
21
+ "cli",
22
+ "agent",
23
+ "skills",
24
+ "remix",
25
+ "devcontainer"
26
+ ],
27
+ "author": "exaedge",
28
+ "license": "ISC",
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ }
32
+ }
package/src/index.js ADDED
@@ -0,0 +1,116 @@
1
+ const { program } = require('commander');
2
+ const prompts = require('prompts');
3
+ const execa = require('execa');
4
+ const fs = require('fs-extra');
5
+ const path = require('path');
6
+ const chalk = require('chalk');
7
+
8
+ program
9
+ .version('1.0.0')
10
+ .description('Create an Agentic Shopify App with pre-configured developer tools')
11
+ .argument('[project-directory]', 'The directory to create the app in')
12
+ .action(async (projectDirectory) => {
13
+ let targetDir = projectDirectory;
14
+
15
+ if (!targetDir) {
16
+ const response = await prompts({
17
+ type: 'text',
18
+ name: 'dirName',
19
+ message: 'What is the name of your new Shopify app?',
20
+ initial: 'my-shopify-app'
21
+ });
22
+ targetDir = response.dirName;
23
+
24
+ if (!targetDir) {
25
+ console.log(chalk.red('Installation cancelled.'));
26
+ process.exit(1);
27
+ }
28
+ }
29
+
30
+ const appPath = path.resolve(process.cwd(), targetDir);
31
+ if (!fs.existsSync(appPath)) {
32
+ console.error(chalk.red(`\nDirectory ${targetDir} does not exist. Please initialize the Shopify project first.\n`));
33
+ process.exit(1);
34
+ }
35
+
36
+ const choices = await prompts({
37
+ type: 'multiselect',
38
+ name: 'features',
39
+ message: 'Which Next-Gen features do you want to add?',
40
+ choices: [
41
+ { title: 'AI Coding Assistant Skills (.agent)', value: 'agent', selected: true },
42
+ { title: 'Pre-configured Dev Environment (.devcontainer)', value: 'devcontainer', selected: true },
43
+ { title: 'Unified Testing Pipeline (Vitest/Playwright)', value: 'testing', selected: true }
44
+ ],
45
+ min: 1,
46
+ hint: '- Space to select. Return to submit'
47
+ });
48
+
49
+ if (!choices.features || choices.features.length === 0) {
50
+ console.log(chalk.yellow('\nNo features selected. Exiting.\n'));
51
+ process.exit(0);
52
+ }
53
+
54
+ console.log(chalk.cyan(`\nšŸš€ Injecting Agentic Developer Tools into /${targetDir}...`));
55
+ try {
56
+ // Assume Shopify App is already initialized in targetDir
57
+
58
+ console.log(chalk.cyan(`\n✨ Project found. Proceeding with injections...\n`));
59
+
60
+ // Step 2: Inject .agent templates
61
+ if (choices.features.includes('agent')) {
62
+ const templateAgentDir = path.join(__dirname, '../.agent');
63
+ if (fs.existsSync(templateAgentDir)) {
64
+ await fs.copy(templateAgentDir, path.join(appPath, '.agent'));
65
+ console.log(chalk.green('āœ“ Injected AI Coding Assistant Skills (.agent)'));
66
+ }
67
+ }
68
+
69
+ // Step 3: Inject Devcontainer
70
+ if (choices.features.includes('devcontainer')) {
71
+ const templateDevcontainer = path.join(__dirname, '../templates/.devcontainer');
72
+ if (fs.existsSync(templateDevcontainer)) {
73
+ await fs.copy(templateDevcontainer, path.join(appPath, '.devcontainer'));
74
+ console.log(chalk.green('āœ“ Injected Devcontainer Configuration'));
75
+ }
76
+ }
77
+
78
+ // Step 4: Inject unified testing
79
+ if (choices.features.includes('testing')) {
80
+ const templateTests = path.join(__dirname, '../templates/tests');
81
+ if (fs.existsSync(templateTests)) {
82
+ await fs.copy(templateTests, appPath);
83
+ console.log(chalk.green('āœ“ Injected Testing Pipeline (Vitest/Playwright config)'));
84
+
85
+ // Update generated package.json with test scripts
86
+ const targetPkgPath = path.join(appPath, 'package.json');
87
+ if (fs.existsSync(targetPkgPath)) {
88
+ const targetPkg = await fs.readJson(targetPkgPath);
89
+ targetPkg.scripts = targetPkg.scripts || {};
90
+ targetPkg.scripts['test'] = 'vitest';
91
+ targetPkg.scripts['test:e2e'] = 'playwright test';
92
+ targetPkg.devDependencies = targetPkg.devDependencies || {};
93
+ targetPkg.devDependencies['vitest'] = '^1.0.0';
94
+ targetPkg.devDependencies['@playwright/test'] = '^1.40.0';
95
+ await fs.writeJson(targetPkgPath, targetPkg, { spaces: 2 });
96
+ }
97
+ }
98
+ }
99
+
100
+ console.log(chalk.cyan(`\nšŸŽ‰ Success! Your Next-Gen Agentic Shopify App is ready.\n`));
101
+ console.log(`Next steps:\n`);
102
+ console.log(` cd ${targetDir}`);
103
+ console.log(` npm install`);
104
+ console.log(` npm run dev\n`);
105
+
106
+ if (choices.features.includes('devcontainer')) {
107
+ console.log(chalk.yellow(`If you are using Cursor or GitHub Codespaces, you can open the project in the Devcontainer now.`));
108
+ }
109
+
110
+ } catch (error) {
111
+ console.error(chalk.red(`\nAn error occurred during creation:\n${error.message}`));
112
+ process.exit(1);
113
+ }
114
+ });
115
+
116
+ program.parse(process.argv);
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: shopify-apps
3
+ description: "Expert patterns for Shopify app development including Remix/React Router apps, embedded apps with App Bridge, webhook handling, GraphQL Admin API, Polaris components, billing, and app extensions. U..."
4
+ risk: unknown
5
+ source: "vibeship-spawner-skills (Apache 2.0)"
6
+ date_added: "2026-02-27"
7
+ ---
8
+
9
+ # Shopify Apps
10
+
11
+ ## Patterns
12
+
13
+ ### React Router App Setup
14
+
15
+ Modern Shopify app template with React Router
16
+
17
+ ### Embedded App with App Bridge
18
+
19
+ Render app embedded in Shopify Admin
20
+
21
+ ### Webhook Handling
22
+
23
+ Secure webhook processing with HMAC verification
24
+
25
+ ## Anti-Patterns
26
+
27
+ ### āŒ REST API for New Apps
28
+
29
+ ### āŒ Webhook Processing Before Response
30
+
31
+ ### āŒ Polling Instead of Webhooks
32
+
33
+ ## āš ļø Sharp Edges
34
+
35
+ | Issue | Severity | Solution |
36
+ |-------|----------|----------|
37
+ | Issue | high | ## Respond immediately, process asynchronously |
38
+ | Issue | high | ## Check rate limit headers |
39
+ | Issue | high | ## Request protected customer data access |
40
+ | Issue | medium | ## Use TOML only (recommended) |
41
+ | Issue | medium | ## Handle both URL formats |
42
+ | Issue | high | ## Use GraphQL for all new code |
43
+ | Issue | high | ## Use latest App Bridge via script tag |
44
+ | Issue | high | ## Implement all GDPR handlers |
45
+
46
+ ## When to Use
47
+ This skill is applicable to execute the workflow or actions described in the overview.