@cloudstreamsoftware/claude-tools 1.0.0 → 1.1.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.
- package/README.md +152 -37
- package/agents/INDEX.md +183 -0
- package/agents/architect.md +247 -0
- package/agents/build-error-resolver.md +555 -0
- package/agents/catalyst-deployer.md +132 -0
- package/agents/code-reviewer.md +121 -0
- package/agents/compliance-auditor.md +148 -0
- package/agents/creator-architect.md +395 -0
- package/agents/deluge-reviewer.md +98 -0
- package/agents/doc-updater.md +471 -0
- package/agents/e2e-runner.md +711 -0
- package/agents/planner.md +122 -0
- package/agents/refactor-cleaner.md +309 -0
- package/agents/security-reviewer.md +582 -0
- package/agents/tdd-guide.md +302 -0
- package/config/versions.json +63 -0
- package/dist/hooks/hooks.json +209 -0
- package/dist/index.js +47 -0
- package/dist/lib/asset-value.js +609 -0
- package/dist/lib/client-manager.js +300 -0
- package/dist/lib/command-matcher.js +242 -0
- package/dist/lib/cross-session-patterns.js +754 -0
- package/dist/lib/intent-classifier.js +1075 -0
- package/dist/lib/package-manager.js +374 -0
- package/dist/lib/recommendation-engine.js +597 -0
- package/dist/lib/session-memory.js +489 -0
- package/dist/lib/skill-effectiveness.js +486 -0
- package/dist/lib/skill-matcher.js +595 -0
- package/dist/lib/tutorial-metrics.js +242 -0
- package/dist/lib/tutorial-progress.js +209 -0
- package/dist/lib/tutorial-renderer.js +431 -0
- package/dist/lib/utils.js +380 -0
- package/dist/lib/verify-formatter.js +143 -0
- package/dist/lib/workflow-state.js +249 -0
- package/hooks/hooks.json +209 -0
- package/package.json +5 -1
- package/scripts/aggregate-sessions.js +290 -0
- package/scripts/branch-name-validator.js +291 -0
- package/scripts/build.js +101 -0
- package/scripts/commands/client-switch.js +231 -0
- package/scripts/deprecate-skill.js +610 -0
- package/scripts/diagnose.js +324 -0
- package/scripts/doc-freshness.js +168 -0
- package/scripts/generate-weekly-digest.js +393 -0
- package/scripts/health-check.js +270 -0
- package/scripts/hooks/credential-check.js +101 -0
- package/scripts/hooks/evaluate-session.js +81 -0
- package/scripts/hooks/pre-compact.js +66 -0
- package/scripts/hooks/prompt-analyzer.js +276 -0
- package/scripts/hooks/prompt-router.js +422 -0
- package/scripts/hooks/quality-gate-enforcer.js +371 -0
- package/scripts/hooks/session-end.js +156 -0
- package/scripts/hooks/session-start.js +195 -0
- package/scripts/hooks/skill-injector.js +333 -0
- package/scripts/hooks/suggest-compact.js +58 -0
- package/scripts/lib/asset-value.js +609 -0
- package/scripts/lib/client-manager.js +300 -0
- package/scripts/lib/command-matcher.js +242 -0
- package/scripts/lib/cross-session-patterns.js +754 -0
- package/scripts/lib/intent-classifier.js +1075 -0
- package/scripts/lib/package-manager.js +374 -0
- package/scripts/lib/recommendation-engine.js +597 -0
- package/scripts/lib/session-memory.js +489 -0
- package/scripts/lib/skill-effectiveness.js +486 -0
- package/scripts/lib/skill-matcher.js +595 -0
- package/scripts/lib/tutorial-metrics.js +242 -0
- package/scripts/lib/tutorial-progress.js +209 -0
- package/scripts/lib/tutorial-renderer.js +431 -0
- package/scripts/lib/utils.js +380 -0
- package/scripts/lib/verify-formatter.js +143 -0
- package/scripts/lib/workflow-state.js +249 -0
- package/scripts/onboard.js +363 -0
- package/scripts/quarterly-report.js +692 -0
- package/scripts/setup-package-manager.js +204 -0
- package/scripts/sync-upstream.js +391 -0
- package/scripts/test.js +108 -0
- package/scripts/tutorial-runner.js +351 -0
- package/scripts/validate-all.js +201 -0
- package/scripts/verifiers/agents.js +245 -0
- package/scripts/verifiers/config.js +186 -0
- package/scripts/verifiers/environment.js +123 -0
- package/scripts/verifiers/hooks.js +188 -0
- package/scripts/verifiers/index.js +38 -0
- package/scripts/verifiers/persistence.js +140 -0
- package/scripts/verifiers/plugin.js +215 -0
- package/scripts/verifiers/skills.js +209 -0
- package/scripts/verify-setup.js +164 -0
- package/skills/INDEX.md +157 -0
- package/skills/backend-patterns/SKILL.md +586 -0
- package/skills/backend-patterns/catalyst-patterns.md +128 -0
- package/skills/bigquery-patterns/SKILL.md +27 -0
- package/skills/bigquery-patterns/performance-optimization.md +518 -0
- package/skills/bigquery-patterns/query-patterns.md +372 -0
- package/skills/bigquery-patterns/schema-design.md +78 -0
- package/skills/cloudstream-project-template/SKILL.md +20 -0
- package/skills/cloudstream-project-template/structure.md +65 -0
- package/skills/coding-standards/SKILL.md +524 -0
- package/skills/coding-standards/deluge-standards.md +83 -0
- package/skills/compliance-patterns/SKILL.md +28 -0
- package/skills/compliance-patterns/hipaa/audit-requirements.md +251 -0
- package/skills/compliance-patterns/hipaa/baa-process.md +298 -0
- package/skills/compliance-patterns/hipaa/data-archival-strategy.md +387 -0
- package/skills/compliance-patterns/hipaa/phi-handling.md +52 -0
- package/skills/compliance-patterns/pci-dss/saq-a-requirements.md +307 -0
- package/skills/compliance-patterns/pci-dss/tokenization-patterns.md +382 -0
- package/skills/compliance-patterns/pci-dss/zoho-checkout-patterns.md +56 -0
- package/skills/compliance-patterns/soc2/access-controls.md +344 -0
- package/skills/compliance-patterns/soc2/audit-logging.md +458 -0
- package/skills/compliance-patterns/soc2/change-management.md +403 -0
- package/skills/compliance-patterns/soc2/deluge-execution-logging.md +407 -0
- package/skills/consultancy-workflows/SKILL.md +19 -0
- package/skills/consultancy-workflows/client-isolation.md +21 -0
- package/skills/consultancy-workflows/documentation-automation.md +454 -0
- package/skills/consultancy-workflows/handoff-procedures.md +257 -0
- package/skills/consultancy-workflows/knowledge-capture.md +513 -0
- package/skills/consultancy-workflows/time-tracking.md +26 -0
- package/skills/continuous-learning/SKILL.md +84 -0
- package/skills/continuous-learning/config.json +18 -0
- package/skills/continuous-learning/evaluate-session.sh +60 -0
- package/skills/continuous-learning-v2/SKILL.md +126 -0
- package/skills/continuous-learning-v2/config.json +61 -0
- package/skills/frontend-patterns/SKILL.md +635 -0
- package/skills/frontend-patterns/zoho-widget-patterns.md +103 -0
- package/skills/gcp-data-engineering/SKILL.md +36 -0
- package/skills/gcp-data-engineering/bigquery/performance-optimization.md +337 -0
- package/skills/gcp-data-engineering/dataflow/error-handling.md +496 -0
- package/skills/gcp-data-engineering/dataflow/pipeline-patterns.md +444 -0
- package/skills/gcp-data-engineering/dbt/model-organization.md +63 -0
- package/skills/gcp-data-engineering/dbt/testing-patterns.md +503 -0
- package/skills/gcp-data-engineering/medallion-architecture/bronze-layer.md +60 -0
- package/skills/gcp-data-engineering/medallion-architecture/gold-layer.md +311 -0
- package/skills/gcp-data-engineering/medallion-architecture/layer-transitions.md +517 -0
- package/skills/gcp-data-engineering/medallion-architecture/silver-layer.md +305 -0
- package/skills/gcp-data-engineering/zoho-to-gcp/data-extraction.md +543 -0
- package/skills/gcp-data-engineering/zoho-to-gcp/real-time-vs-batch.md +337 -0
- package/skills/security-review/SKILL.md +498 -0
- package/skills/security-review/compliance-checklist.md +53 -0
- package/skills/strategic-compact/SKILL.md +67 -0
- package/skills/tdd-workflow/SKILL.md +413 -0
- package/skills/tdd-workflow/zoho-testing.md +124 -0
- package/skills/tutorial/SKILL.md +249 -0
- package/skills/tutorial/docs/ACCESSIBILITY.md +169 -0
- package/skills/tutorial/lessons/00-philosophy-and-workflow.md +198 -0
- package/skills/tutorial/lessons/01-basics.md +81 -0
- package/skills/tutorial/lessons/02-training.md +86 -0
- package/skills/tutorial/lessons/03-commands.md +109 -0
- package/skills/tutorial/lessons/04-workflows.md +115 -0
- package/skills/tutorial/lessons/05-compliance.md +116 -0
- package/skills/tutorial/lessons/06-zoho.md +121 -0
- package/skills/tutorial/lessons/07-hooks-system.md +277 -0
- package/skills/tutorial/lessons/08-mcp-servers.md +316 -0
- package/skills/tutorial/lessons/09-client-management.md +215 -0
- package/skills/tutorial/lessons/10-testing-e2e.md +260 -0
- package/skills/tutorial/lessons/11-skills-deep-dive.md +272 -0
- package/skills/tutorial/lessons/12-rules-system.md +326 -0
- package/skills/tutorial/lessons/13-golden-standard-graduation.md +213 -0
- package/skills/tutorial/lessons/14-fork-setup-and-sync.md +312 -0
- package/skills/tutorial/lessons/15-living-examples-system.md +221 -0
- package/skills/tutorial/tracks/accelerated/README.md +134 -0
- package/skills/tutorial/tracks/accelerated/assessment/checkpoint-1.md +161 -0
- package/skills/tutorial/tracks/accelerated/assessment/checkpoint-2.md +175 -0
- package/skills/tutorial/tracks/accelerated/day-1-core-concepts.md +234 -0
- package/skills/tutorial/tracks/accelerated/day-2-essential-commands.md +270 -0
- package/skills/tutorial/tracks/accelerated/day-3-workflow-mastery.md +305 -0
- package/skills/tutorial/tracks/accelerated/day-4-compliance-zoho.md +304 -0
- package/skills/tutorial/tracks/accelerated/day-5-hooks-skills.md +344 -0
- package/skills/tutorial/tracks/accelerated/day-6-client-testing.md +386 -0
- package/skills/tutorial/tracks/accelerated/day-7-graduation.md +369 -0
- package/skills/zoho-patterns/CHANGELOG.md +108 -0
- package/skills/zoho-patterns/SKILL.md +446 -0
- package/skills/zoho-patterns/analytics/dashboard-patterns.md +352 -0
- package/skills/zoho-patterns/analytics/zoho-to-bigquery-pipeline.md +427 -0
- package/skills/zoho-patterns/catalyst/appsail-deployment.md +349 -0
- package/skills/zoho-patterns/catalyst/context-close-patterns.md +354 -0
- package/skills/zoho-patterns/catalyst/cron-batch-processing.md +374 -0
- package/skills/zoho-patterns/catalyst/function-patterns.md +439 -0
- package/skills/zoho-patterns/creator/form-design.md +304 -0
- package/skills/zoho-patterns/creator/publish-api-patterns.md +313 -0
- package/skills/zoho-patterns/creator/widget-integration.md +306 -0
- package/skills/zoho-patterns/creator/workflow-automation.md +253 -0
- package/skills/zoho-patterns/deluge/api-patterns.md +468 -0
- package/skills/zoho-patterns/deluge/batch-processing.md +403 -0
- package/skills/zoho-patterns/deluge/cross-app-integration.md +356 -0
- package/skills/zoho-patterns/deluge/error-handling.md +423 -0
- package/skills/zoho-patterns/deluge/syntax-reference.md +65 -0
- package/skills/zoho-patterns/integration/cors-proxy-architecture.md +426 -0
- package/skills/zoho-patterns/integration/crm-books-native-sync.md +277 -0
- package/skills/zoho-patterns/integration/oauth-token-management.md +461 -0
- package/skills/zoho-patterns/integration/zoho-flow-patterns.md +334 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# CORS Proxy Architecture
|
|
2
|
+
|
|
3
|
+
## Why Widgets Can't Call External APIs Directly
|
|
4
|
+
|
|
5
|
+
> **PROBLEM:** Creator widgets run inside an iframe on `*.zoho.com`. Browser CORS policy blocks cross-origin requests to external APIs. Even if the external API has CORS headers, they won't include Zoho's origin.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Widget (iframe on zoho.com)
|
|
9
|
+
↓ fetch("https://api.stripe.com/...")
|
|
10
|
+
✗ BLOCKED by browser CORS policy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## The Solution: Catalyst Proxy
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Widget (iframe)
|
|
17
|
+
↓ ZOHO.CREATOR.DATA.invokeFunction() [Same origin - no CORS]
|
|
18
|
+
↓
|
|
19
|
+
Catalyst Function (server-side)
|
|
20
|
+
↓ axios/fetch to external API [Server-side - no CORS]
|
|
21
|
+
↓
|
|
22
|
+
External API
|
|
23
|
+
↓ Response
|
|
24
|
+
↓
|
|
25
|
+
Catalyst Function
|
|
26
|
+
↓ Transform + return
|
|
27
|
+
↓
|
|
28
|
+
Widget (receives data)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Architecture Overview
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌─────────────────────────────────────────────────────────┐
|
|
35
|
+
│ Creator Widget (Browser) │
|
|
36
|
+
│ │
|
|
37
|
+
│ ┌─────────────────────────────────────────────────┐ │
|
|
38
|
+
│ │ ZOHO.CREATOR.DATA.invokeFunction({ │ │
|
|
39
|
+
│ │ functionName: "proxy_api", │ │
|
|
40
|
+
│ │ data: { endpoint, method, body, headers } │ │
|
|
41
|
+
│ │ }) │ │
|
|
42
|
+
│ └─────────────────┬───────────────────────────────┘ │
|
|
43
|
+
│ │ (Same Zoho domain = OK) │
|
|
44
|
+
└─────────────────────┼───────────────────────────────────┘
|
|
45
|
+
│
|
|
46
|
+
┌─────────────────────▼───────────────────────────────────┐
|
|
47
|
+
│ Catalyst I/O Function (Server) │
|
|
48
|
+
│ │
|
|
49
|
+
│ - Validates request │
|
|
50
|
+
│ - Adds auth credentials (from env vars) │
|
|
51
|
+
│ - Calls external API │
|
|
52
|
+
│ - Transforms response │
|
|
53
|
+
│ - Returns to widget │
|
|
54
|
+
│ │
|
|
55
|
+
│ ⚠️ 30-second timeout for I/O functions │
|
|
56
|
+
│ ⚠️ context.close() MANDATORY │
|
|
57
|
+
└─────────────────────┬───────────────────────────────────┘
|
|
58
|
+
│
|
|
59
|
+
┌─────────────────────▼───────────────────────────────────┐
|
|
60
|
+
│ External API (Stripe, Twilio, Google, etc.) │
|
|
61
|
+
└─────────────────────────────────────────────────────────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Catalyst Proxy Function Implementation
|
|
65
|
+
|
|
66
|
+
### Basic Proxy (Node.js)
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
const catalyst = require("zcatalyst-sdk-node");
|
|
70
|
+
const axios = require("axios");
|
|
71
|
+
|
|
72
|
+
module.exports = async (req, res, context) => {
|
|
73
|
+
const app = catalyst.initialize(context);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Validate request
|
|
77
|
+
const { endpoint, method, body, headers: customHeaders } = req.body;
|
|
78
|
+
|
|
79
|
+
if (!endpoint) {
|
|
80
|
+
res.status(400).json({ error: "endpoint is required" });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Whitelist allowed endpoints (SECURITY: prevent SSRF)
|
|
85
|
+
const allowedDomains = [
|
|
86
|
+
"api.stripe.com",
|
|
87
|
+
"api.twilio.com",
|
|
88
|
+
"maps.googleapis.com"
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const url = new URL(endpoint);
|
|
92
|
+
if (!allowedDomains.includes(url.hostname)) {
|
|
93
|
+
res.status(403).json({ error: "Domain not allowed: " + url.hostname });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Build request
|
|
98
|
+
const axiosConfig = {
|
|
99
|
+
method: method || "GET",
|
|
100
|
+
url: endpoint,
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
...customHeaders
|
|
104
|
+
},
|
|
105
|
+
timeout: 25000 // 25s (buffer for 30s Catalyst timeout)
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
|
|
109
|
+
axiosConfig.data = typeof body === "string" ? JSON.parse(body) : body;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add server-side credentials
|
|
113
|
+
axiosConfig.headers["Authorization"] = getAuthForDomain(url.hostname);
|
|
114
|
+
|
|
115
|
+
// Execute request
|
|
116
|
+
const response = await axios(axiosConfig);
|
|
117
|
+
|
|
118
|
+
res.status(200).json({
|
|
119
|
+
status: response.status,
|
|
120
|
+
data: response.data
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error.response) {
|
|
125
|
+
// External API returned an error
|
|
126
|
+
res.status(200).json({
|
|
127
|
+
status: error.response.status,
|
|
128
|
+
error: error.response.data
|
|
129
|
+
});
|
|
130
|
+
} else if (error.code === "ECONNABORTED") {
|
|
131
|
+
res.status(504).json({ error: "External API timeout" });
|
|
132
|
+
} else {
|
|
133
|
+
res.status(500).json({ error: error.message });
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
context.close();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function getAuthForDomain(domain) {
|
|
141
|
+
const authMap = {
|
|
142
|
+
"api.stripe.com": `Bearer ${process.env.STRIPE_SECRET_KEY}`,
|
|
143
|
+
"api.twilio.com": `Basic ${Buffer.from(process.env.TWILIO_SID + ":" + process.env.TWILIO_TOKEN).toString("base64")}`,
|
|
144
|
+
"maps.googleapis.com": "" // Uses query param key instead
|
|
145
|
+
};
|
|
146
|
+
return authMap[domain] || "";
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Widget-Side Code
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
// Generic proxy caller for widgets
|
|
154
|
+
async function proxyApiCall(endpoint, method = "GET", body = null) {
|
|
155
|
+
try {
|
|
156
|
+
const response = await ZOHO.CREATOR.DATA.invokeFunction({
|
|
157
|
+
appName: "my-app",
|
|
158
|
+
functionName: "proxy_api",
|
|
159
|
+
data: {
|
|
160
|
+
endpoint: endpoint,
|
|
161
|
+
method: method,
|
|
162
|
+
body: body ? JSON.stringify(body) : null
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = JSON.parse(response.data);
|
|
167
|
+
|
|
168
|
+
if (result.status >= 200 && result.status < 300) {
|
|
169
|
+
return { success: true, data: result.data };
|
|
170
|
+
} else {
|
|
171
|
+
return { success: false, error: result.error || result.data };
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error("Proxy call failed:", error);
|
|
175
|
+
return { success: false, error: error.message };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Usage examples
|
|
180
|
+
async function getStripeCustomer(customerId) {
|
|
181
|
+
return await proxyApiCall(
|
|
182
|
+
`https://api.stripe.com/v1/customers/${customerId}`,
|
|
183
|
+
"GET"
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function sendTwilioSMS(to, message) {
|
|
188
|
+
return await proxyApiCall(
|
|
189
|
+
`https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`,
|
|
190
|
+
"POST",
|
|
191
|
+
{ To: to, From: TWILIO_NUMBER, Body: message }
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Authentication Passthrough
|
|
197
|
+
|
|
198
|
+
### Pattern 1: Server-Side Credentials (Recommended)
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
// Credentials stored in Catalyst environment variables
|
|
202
|
+
// Widget NEVER sees API keys
|
|
203
|
+
function getAuthForDomain(domain) {
|
|
204
|
+
switch (domain) {
|
|
205
|
+
case "api.stripe.com":
|
|
206
|
+
return `Bearer ${process.env.STRIPE_SECRET_KEY}`;
|
|
207
|
+
case "api.openai.com":
|
|
208
|
+
return `Bearer ${process.env.OPENAI_API_KEY}`;
|
|
209
|
+
default:
|
|
210
|
+
return "";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Pattern 2: User-Specific Tokens (OAuth)
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
// For APIs where each user has their own token
|
|
219
|
+
// Store user tokens in Catalyst Data Store
|
|
220
|
+
async function getUserToken(app, userId, service) {
|
|
221
|
+
const result = await app.zcql().executeZCQLQuery(
|
|
222
|
+
`SELECT access_token, refresh_token, expires_at FROM UserTokens WHERE user_id = '${userId}' AND service = '${service}'`
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (result.length === 0) {
|
|
226
|
+
throw new Error("No token found. User must authenticate first.");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const tokenData = result[0].UserTokens;
|
|
230
|
+
|
|
231
|
+
// Check if token expired
|
|
232
|
+
if (new Date(tokenData.expires_at) < new Date()) {
|
|
233
|
+
// Refresh token
|
|
234
|
+
return await refreshUserToken(app, userId, service, tokenData.refresh_token);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return tokenData.access_token;
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Response Transformation
|
|
242
|
+
|
|
243
|
+
Transform external API responses to match widget expectations:
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
// Transform Stripe response to simplified format for widget
|
|
247
|
+
function transformStripeCustomer(stripeData) {
|
|
248
|
+
return {
|
|
249
|
+
id: stripeData.id,
|
|
250
|
+
name: stripeData.name,
|
|
251
|
+
email: stripeData.email,
|
|
252
|
+
balance: stripeData.balance / 100, // Cents to dollars
|
|
253
|
+
subscriptions: (stripeData.subscriptions?.data || []).map(sub => ({
|
|
254
|
+
id: sub.id,
|
|
255
|
+
plan: sub.plan.nickname,
|
|
256
|
+
status: sub.status,
|
|
257
|
+
amount: sub.plan.amount / 100,
|
|
258
|
+
interval: sub.plan.interval
|
|
259
|
+
})),
|
|
260
|
+
created: new Date(stripeData.created * 1000).toISOString()
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Caching Layer
|
|
266
|
+
|
|
267
|
+
Reduce external API calls with caching:
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
const NodeCache = require("node-cache");
|
|
271
|
+
const cache = new NodeCache({ stdTTL: 300 }); // 5-minute default TTL
|
|
272
|
+
|
|
273
|
+
async function cachedProxyCall(endpoint, method, body, cacheTTL = 300) {
|
|
274
|
+
// Only cache GET requests
|
|
275
|
+
if (method === "GET") {
|
|
276
|
+
const cacheKey = `proxy:${endpoint}`;
|
|
277
|
+
const cached = cache.get(cacheKey);
|
|
278
|
+
|
|
279
|
+
if (cached) {
|
|
280
|
+
console.log("Cache hit:", endpoint);
|
|
281
|
+
return cached;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Make actual API call
|
|
286
|
+
const response = await axios({ method, url: endpoint, data: body });
|
|
287
|
+
|
|
288
|
+
// Cache successful GET responses
|
|
289
|
+
if (method === "GET" && response.status === 200) {
|
|
290
|
+
cache.set(`proxy:${endpoint}`, response.data, cacheTTL);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return response.data;
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
> **NOTE:** In-memory cache resets on function cold starts. For persistent caching, use Catalyst Data Store or Cache service.
|
|
298
|
+
|
|
299
|
+
## Rate Limiting at Proxy Level
|
|
300
|
+
|
|
301
|
+
```javascript
|
|
302
|
+
// Simple rate limiter per client/endpoint
|
|
303
|
+
const rateLimits = {};
|
|
304
|
+
const WINDOW_MS = 60000; // 1 minute window
|
|
305
|
+
const MAX_REQUESTS = 30; // 30 requests per minute per endpoint
|
|
306
|
+
|
|
307
|
+
function checkRateLimit(endpoint, userId) {
|
|
308
|
+
const key = `${userId}:${endpoint}`;
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
|
|
311
|
+
if (!rateLimits[key]) {
|
|
312
|
+
rateLimits[key] = { count: 0, windowStart: now };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const limit = rateLimits[key];
|
|
316
|
+
|
|
317
|
+
// Reset window if expired
|
|
318
|
+
if (now - limit.windowStart > WINDOW_MS) {
|
|
319
|
+
limit.count = 0;
|
|
320
|
+
limit.windowStart = now;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
limit.count++;
|
|
324
|
+
|
|
325
|
+
if (limit.count > MAX_REQUESTS) {
|
|
326
|
+
return { allowed: false, retryAfter: Math.ceil((WINDOW_MS - (now - limit.windowStart)) / 1000) };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { allowed: true, remaining: MAX_REQUESTS - limit.count };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Usage in handler
|
|
333
|
+
const rateCheck = checkRateLimit(endpoint, req.headers["x-user-id"] || "anonymous");
|
|
334
|
+
if (!rateCheck.allowed) {
|
|
335
|
+
res.status(429).json({
|
|
336
|
+
error: "Rate limit exceeded",
|
|
337
|
+
retry_after: rateCheck.retryAfter
|
|
338
|
+
});
|
|
339
|
+
context.close();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Security Considerations
|
|
345
|
+
|
|
346
|
+
> **WARNING:** A proxy that forwards arbitrary URLs is an SSRF vulnerability. Always implement:
|
|
347
|
+
|
|
348
|
+
1. **Domain whitelist** - Only allow known, approved external domains
|
|
349
|
+
2. **Method restrictions** - Only allow needed HTTP methods
|
|
350
|
+
3. **Request size limits** - Prevent large payload attacks
|
|
351
|
+
4. **Authentication** - Verify the widget caller is a legitimate Zoho user
|
|
352
|
+
5. **Logging** - Log all proxy requests for audit trail
|
|
353
|
+
6. **No credential exposure** - Never return API keys to the client
|
|
354
|
+
|
|
355
|
+
### SSRF Prevention
|
|
356
|
+
|
|
357
|
+
```javascript
|
|
358
|
+
// NEVER do this:
|
|
359
|
+
const response = await axios.get(req.body.url); // Attacker can access internal network!
|
|
360
|
+
|
|
361
|
+
// ALWAYS do this:
|
|
362
|
+
const allowedDomains = ["api.stripe.com", "api.twilio.com"];
|
|
363
|
+
const url = new URL(req.body.endpoint);
|
|
364
|
+
|
|
365
|
+
if (!allowedDomains.includes(url.hostname)) {
|
|
366
|
+
res.status(403).json({ error: "Forbidden domain" });
|
|
367
|
+
context.close();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Also block internal IPs
|
|
372
|
+
const blockedPatterns = ["127.0.0.1", "localhost", "10.", "172.16.", "192.168.", "169.254."];
|
|
373
|
+
if (blockedPatterns.some(p => url.hostname.startsWith(p))) {
|
|
374
|
+
res.status(403).json({ error: "Internal addresses not allowed" });
|
|
375
|
+
context.close();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Alternative: Deluge Custom Function as Proxy
|
|
381
|
+
|
|
382
|
+
For simpler cases where Catalyst is not needed:
|
|
383
|
+
|
|
384
|
+
```deluge
|
|
385
|
+
// Creator custom function: proxy_api_call
|
|
386
|
+
// Arguments: endpoint (TEXT), method (TEXT), payload (TEXT)
|
|
387
|
+
|
|
388
|
+
// Whitelist check
|
|
389
|
+
allowedDomains = {"api.stripe.com", "api.example.com"};
|
|
390
|
+
urlDomain = endpoint.getPrefix("//").getSuffix("//").getPrefix("/");
|
|
391
|
+
|
|
392
|
+
if (!allowedDomains.contains(urlDomain))
|
|
393
|
+
{
|
|
394
|
+
return {"status": "error", "message": "Domain not allowed"};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
try
|
|
398
|
+
{
|
|
399
|
+
if (method == "GET")
|
|
400
|
+
{
|
|
401
|
+
response = invokeUrl [
|
|
402
|
+
url: endpoint
|
|
403
|
+
type: GET
|
|
404
|
+
connection: "external-api-connection"
|
|
405
|
+
];
|
|
406
|
+
}
|
|
407
|
+
else
|
|
408
|
+
{
|
|
409
|
+
response = invokeUrl [
|
|
410
|
+
url: endpoint
|
|
411
|
+
type: POST
|
|
412
|
+
headers: {"Content-Type": "application/json"}
|
|
413
|
+
body: payload
|
|
414
|
+
connection: "external-api-connection"
|
|
415
|
+
];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {"status": "success", "data": response.toString()};
|
|
419
|
+
}
|
|
420
|
+
catch (e)
|
|
421
|
+
{
|
|
422
|
+
return {"status": "error", "message": e.toString()};
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
> **TRADE-OFF:** Deluge proxy is simpler but limited to 40-second timeout and uses the 5000-statement budget. Catalyst proxy has 30-second timeout but is more flexible for complex transformations.
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# CRM-Books Native Sync
|
|
2
|
+
|
|
3
|
+
> Last verified: 2026-01 (Zoho CRM v5 API, Books API v3)
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
> **CRITICAL:** Zoho CRM and Zoho Books have a built-in native sync that runs on a 2-hour cycle. Before building custom sync logic, verify the native sync doesn't already cover your use case. Rebuilding what already exists creates data conflicts, maintenance burden, and billing waste.
|
|
8
|
+
|
|
9
|
+
## What Syncs Automatically
|
|
10
|
+
|
|
11
|
+
### CRM → Books (Entities)
|
|
12
|
+
|
|
13
|
+
| CRM Entity | Books Entity | Sync Direction | Notes |
|
|
14
|
+
|-----------|-------------|----------------|-------|
|
|
15
|
+
| Accounts | Customers | CRM → Books | Company records |
|
|
16
|
+
| Contacts | Contact Persons | CRM → Books | Individual contacts |
|
|
17
|
+
| Products | Items | Bidirectional | Shared catalog |
|
|
18
|
+
| Quotes | Estimates | CRM → Books | When accepted |
|
|
19
|
+
| Sales Orders | Sales Orders | CRM → Books | When confirmed |
|
|
20
|
+
| Invoices | Invoices | CRM → Books | When created |
|
|
21
|
+
| Purchase Orders | Purchase Orders | CRM → Books | When issued |
|
|
22
|
+
|
|
23
|
+
### Fields That Sync
|
|
24
|
+
|
|
25
|
+
| Category | Fields | Notes |
|
|
26
|
+
|----------|--------|-------|
|
|
27
|
+
| Contact Info | Name, Email, Phone, Address | All standard fields |
|
|
28
|
+
| Financial | Amount, Tax, Discount | Line item details |
|
|
29
|
+
| Products | Name, SKU, Price, Description | Full catalog sync |
|
|
30
|
+
| Status | Invoice status, Payment status | Read from Books |
|
|
31
|
+
| Custom Fields | Only if mapped in sync settings | Manual configuration required |
|
|
32
|
+
|
|
33
|
+
### What Does NOT Sync Automatically
|
|
34
|
+
|
|
35
|
+
- **Custom modules** in CRM
|
|
36
|
+
- **Activities** (Tasks, Events, Calls)
|
|
37
|
+
- **Notes** and attachments
|
|
38
|
+
- **Custom fields** unless explicitly mapped
|
|
39
|
+
- **Deals/Opportunities** (only Quotes/Orders/Invoices)
|
|
40
|
+
- **Vendors** (must configure separately)
|
|
41
|
+
- **Payment receipts** and credit notes (Books → CRM)
|
|
42
|
+
- **Expense records**
|
|
43
|
+
- **Timesheet data**
|
|
44
|
+
|
|
45
|
+
## Sync Cycle Details
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Every 2 Hours:
|
|
49
|
+
┌─────────────────────────────────────────┐
|
|
50
|
+
│ 1. CRM checks for modified records │
|
|
51
|
+
│ 2. Compares with last sync timestamp │
|
|
52
|
+
│ 3. Pushes changed records to Books │
|
|
53
|
+
│ 4. Books processes and confirms │
|
|
54
|
+
│ 5. Updates sync status in CRM │
|
|
55
|
+
│ 6. Next cycle scheduled │
|
|
56
|
+
└─────────────────────────────────────────┘
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Sync Timing
|
|
60
|
+
|
|
61
|
+
- Default interval: Every 2 hours
|
|
62
|
+
- Manual sync: Available in CRM Settings > Zoho Finance > Sync Now
|
|
63
|
+
- First sync: Can take longer for large datasets
|
|
64
|
+
- Conflict resolution: Last-modified-wins (CRM takes priority on shared fields)
|
|
65
|
+
|
|
66
|
+
## When to NOT Rebuild This
|
|
67
|
+
|
|
68
|
+
> **RULE:** If the native sync handles your data flow, do NOT create custom Deluge scripts that duplicate it. Custom sync will conflict with native sync and cause data inconsistencies.
|
|
69
|
+
|
|
70
|
+
### Scenarios Where Native Sync is Sufficient
|
|
71
|
+
|
|
72
|
+
1. Standard contact/customer sync between CRM and Books
|
|
73
|
+
2. Product catalog shared between both apps
|
|
74
|
+
3. Quote-to-Invoice conversion flow
|
|
75
|
+
4. Basic sales order processing
|
|
76
|
+
5. Standard invoice creation from CRM deals
|
|
77
|
+
|
|
78
|
+
### Decision Framework
|
|
79
|
+
|
|
80
|
+
| Need | Native Sync | Extend Native | Build Custom |
|
|
81
|
+
|------|-------------|---------------|-------------|
|
|
82
|
+
| Contacts CRM→Books | Use native | - | - |
|
|
83
|
+
| Real-time sync (< 5 min) | No | No | Yes (webhook) |
|
|
84
|
+
| Custom field mapping | - | Yes (configure) | - |
|
|
85
|
+
| Conditional sync (filter) | No | No | Yes |
|
|
86
|
+
| Transform data during sync | No | No | Yes |
|
|
87
|
+
| Multi-org sync | No | No | Yes |
|
|
88
|
+
| Custom module sync | No | No | Yes |
|
|
89
|
+
| Sync with non-Zoho app | No | No | Yes |
|
|
90
|
+
| Payment status → CRM | Partial | Yes | Maybe |
|
|
91
|
+
|
|
92
|
+
## How to Extend (Not Replace) Native Sync
|
|
93
|
+
|
|
94
|
+
### Adding Custom Field Mapping
|
|
95
|
+
|
|
96
|
+
1. Go to CRM > Settings > Zoho Finance Suite
|
|
97
|
+
2. Click "Sync Preferences"
|
|
98
|
+
3. Map additional fields under "Field Mapping"
|
|
99
|
+
4. Custom fields must exist in BOTH apps first
|
|
100
|
+
|
|
101
|
+
### Supplementing with Workflow Triggers
|
|
102
|
+
|
|
103
|
+
```deluge
|
|
104
|
+
// In CRM: On Edit of Contact, push extra fields to Books
|
|
105
|
+
// This SUPPLEMENTS native sync (doesn't replace it)
|
|
106
|
+
// Only sync fields that native sync DOESN'T handle
|
|
107
|
+
|
|
108
|
+
if (input.Custom_Field_1 != input.Custom_Field_1_previous)
|
|
109
|
+
{
|
|
110
|
+
// Find corresponding Books contact
|
|
111
|
+
try
|
|
112
|
+
{
|
|
113
|
+
booksContacts = zoho.books.getRecords("contacts", orgId, {
|
|
114
|
+
"email": input.Email
|
|
115
|
+
}, "books-connection");
|
|
116
|
+
|
|
117
|
+
if (booksContacts.get("contacts").size() > 0)
|
|
118
|
+
{
|
|
119
|
+
booksContactId = booksContacts.get("contacts").get(0).get("contact_id");
|
|
120
|
+
|
|
121
|
+
// Update ONLY custom fields (let native sync handle standard fields)
|
|
122
|
+
updateData = Map();
|
|
123
|
+
updateData.put("cf_custom_field_1", input.Custom_Field_1);
|
|
124
|
+
updateData.put("cf_custom_field_2", input.Custom_Field_2);
|
|
125
|
+
|
|
126
|
+
zoho.books.updateRecord("contacts", orgId, booksContactId, updateData, "books-connection");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (e)
|
|
130
|
+
{
|
|
131
|
+
info "Custom field sync failed: " + e.toString();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Triggering Actions After Sync
|
|
137
|
+
|
|
138
|
+
```deluge
|
|
139
|
+
// Scheduled function: Check for recently synced records and post-process
|
|
140
|
+
// Runs every 30 minutes to catch records synced by native 2-hour cycle
|
|
141
|
+
|
|
142
|
+
lastCheck = zoho.creator.getRecords("admin-app", "Sync_State", "Key == \"last_books_check\"", 1, 1);
|
|
143
|
+
lastCheckTime = lastCheck.get(0).get("Value");
|
|
144
|
+
|
|
145
|
+
// Get recently created invoices in Books (created by native sync)
|
|
146
|
+
try
|
|
147
|
+
{
|
|
148
|
+
newInvoices = zoho.books.getRecords("invoices", orgId, {
|
|
149
|
+
"date_start": lastCheckTime,
|
|
150
|
+
"date_end": zoho.currentdate.toString("yyyy-MM-dd"),
|
|
151
|
+
"status": "draft"
|
|
152
|
+
}, "books-connection");
|
|
153
|
+
|
|
154
|
+
for each invoice in newInvoices.get("invoices")
|
|
155
|
+
{
|
|
156
|
+
// Post-processing: auto-send draft invoices over $100
|
|
157
|
+
if (invoice.get("total").toDecimal() > 100)
|
|
158
|
+
{
|
|
159
|
+
zoho.books.updateRecord("invoices", orgId, invoice.get("invoice_id"),
|
|
160
|
+
{"status": "sent"}, "books-connection");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (e)
|
|
165
|
+
{
|
|
166
|
+
info "Post-sync processing failed: " + e.toString();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Update last check timestamp
|
|
170
|
+
zoho.creator.updateRecord("admin-app", "Sync_State",
|
|
171
|
+
lastCheck.get(0).get("ID").toLong(), {"Value": zoho.currenttime.toString()});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Sync Status Checking
|
|
175
|
+
|
|
176
|
+
### Check Sync Status in CRM
|
|
177
|
+
|
|
178
|
+
```deluge
|
|
179
|
+
// Get sync status for a specific record
|
|
180
|
+
function checkSyncStatus(module, recordId)
|
|
181
|
+
{
|
|
182
|
+
try
|
|
183
|
+
{
|
|
184
|
+
record = zoho.crm.getRecordById(module, recordId);
|
|
185
|
+
|
|
186
|
+
// Check Books-related fields
|
|
187
|
+
booksId = record.get("Books_Contact_ID"); // Populated after sync
|
|
188
|
+
lastSync = record.get("Last_Synced_Time"); // If available
|
|
189
|
+
|
|
190
|
+
if (booksId != null && booksId != "")
|
|
191
|
+
{
|
|
192
|
+
return {"synced": true, "books_id": booksId, "last_sync": lastSync};
|
|
193
|
+
}
|
|
194
|
+
else
|
|
195
|
+
{
|
|
196
|
+
return {"synced": false, "reason": "Not yet synced or sync failed"};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (e)
|
|
200
|
+
{
|
|
201
|
+
return {"synced": false, "reason": e.toString()};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Monitor Sync Health
|
|
207
|
+
|
|
208
|
+
```deluge
|
|
209
|
+
// Scheduled: Daily check for sync issues
|
|
210
|
+
// Find CRM records that should have synced but haven't
|
|
211
|
+
|
|
212
|
+
criteria = "Created_Time < \"" + zoho.currentdate.subDay(1).toString("yyyy-MM-dd") + "\" && Books_Contact_ID is null";
|
|
213
|
+
unsyncedContacts = zoho.crm.getRecords("Contacts", criteria, 1, 200);
|
|
214
|
+
|
|
215
|
+
if (unsyncedContacts.size() > 10)
|
|
216
|
+
{
|
|
217
|
+
sendmail [
|
|
218
|
+
from: zoho.adminuserid
|
|
219
|
+
to: "admin@company.com"
|
|
220
|
+
subject: "[WARNING] " + unsyncedContacts.size() + " contacts not synced to Books"
|
|
221
|
+
message: "Found " + unsyncedContacts.size() + " contacts created over 24 hours ago that have not synced to Books. Check sync configuration."
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Troubleshooting Sync Failures
|
|
227
|
+
|
|
228
|
+
| Symptom | Likely Cause | Fix |
|
|
229
|
+
|---------|-------------|-----|
|
|
230
|
+
| Records not syncing | Sync disabled in settings | CRM Settings > Zoho Finance > Enable |
|
|
231
|
+
| Only some records sync | Filter criteria in sync settings | Review sync filters |
|
|
232
|
+
| Custom fields missing | Fields not mapped | Add mapping in sync preferences |
|
|
233
|
+
| Duplicate records in Books | Multiple sync sources configured | Disable duplicate sources |
|
|
234
|
+
| Sync errors in log | Field validation failure in Books | Check Books mandatory fields |
|
|
235
|
+
| Payment status not updating | Books→CRM sync disabled | Enable bidirectional sync |
|
|
236
|
+
| Products not syncing | Product sync not enabled | Enable in sync preferences |
|
|
237
|
+
|
|
238
|
+
### Sync Error Log Location
|
|
239
|
+
|
|
240
|
+
1. CRM: Settings > Zoho Finance > Sync History
|
|
241
|
+
2. Books: Settings > Integrations > Zoho CRM > Sync History
|
|
242
|
+
3. Logs retained for 90 days
|
|
243
|
+
|
|
244
|
+
### Common Sync Errors
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
Error: "Contact already exists in Books"
|
|
248
|
+
→ Duplicate email address. Merge contacts in Books first.
|
|
249
|
+
|
|
250
|
+
Error: "Mandatory field missing"
|
|
251
|
+
→ Books requires fields that CRM doesn't. Set defaults in Books.
|
|
252
|
+
|
|
253
|
+
Error: "Currency mismatch"
|
|
254
|
+
→ Multi-currency not enabled in Books. Enable or map currencies.
|
|
255
|
+
|
|
256
|
+
Error: "Tax rate not found"
|
|
257
|
+
→ Tax configuration differs between apps. Create matching tax rates.
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Integration Architecture Recommendation
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
For Standard Flows:
|
|
264
|
+
CRM ──[Native 2h Sync]──→ Books
|
|
265
|
+
└──[Supplement: Custom fields via workflow]──→ Books
|
|
266
|
+
|
|
267
|
+
For Real-Time Needs:
|
|
268
|
+
CRM ──[On-Edit Workflow]──→ Catalyst Function ──→ Books API
|
|
269
|
+
(Only for fields/entities NOT covered by native sync)
|
|
270
|
+
|
|
271
|
+
For Reporting:
|
|
272
|
+
CRM ──[Native Sync]──→ Books
|
|
273
|
+
Books ──[Analytics Sync]──→ Zoho Analytics
|
|
274
|
+
(Do NOT pull directly from CRM if Books has the data)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
> **FINAL WARNING:** If you find yourself writing Deluge code that syncs Contacts, Accounts, Products, or Invoices between CRM and Books - STOP. The native sync handles this. You are likely duplicating built-in functionality and creating future conflicts.
|