@cloudstreamsoftware/claude-tools 1.0.0 → 1.2.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/bin/cloudstream-setup.js +16 -6
- 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,461 @@
|
|
|
1
|
+
# OAuth Token Management
|
|
2
|
+
|
|
3
|
+
## Zoho OAuth Token Lifecycle
|
|
4
|
+
|
|
5
|
+
> **CRITICAL:** Zoho access tokens expire in exactly 1 hour (3600 seconds). You MUST implement refresh logic for any standalone application. Deluge Connections handle this automatically.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Authorization Flow:
|
|
9
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
10
|
+
│ User grants │ → │ Auth code │ → │ Access token │
|
|
11
|
+
│ permission │ │ (10 min TTL) │ │ (1 hour TTL) │
|
|
12
|
+
└──────────────┘ └──────────────┘ └──────┬───────┘
|
|
13
|
+
│ Expires
|
|
14
|
+
↓
|
|
15
|
+
┌──────────────┐
|
|
16
|
+
│ Refresh token│ → New access token
|
|
17
|
+
│ (no expiry*) │
|
|
18
|
+
└──────────────┘
|
|
19
|
+
* Refresh tokens don't expire but can be revoked
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Using Connections in Deluge (Preferred)
|
|
23
|
+
|
|
24
|
+
Connections handle token refresh automatically. This is always the preferred approach within Zoho apps:
|
|
25
|
+
|
|
26
|
+
```deluge
|
|
27
|
+
// Connections manage the full OAuth lifecycle
|
|
28
|
+
// You NEVER see or handle tokens directly
|
|
29
|
+
response = invokeUrl [
|
|
30
|
+
url: "https://www.zohoapis.com/crm/v5/Deals"
|
|
31
|
+
type: GET
|
|
32
|
+
connection: "zoho-crm-connection"
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// The connection handles:
|
|
36
|
+
// 1. Storing the access token
|
|
37
|
+
// 2. Detecting expiry
|
|
38
|
+
// 3. Using refresh token to get new access token
|
|
39
|
+
// 4. Retrying the request with new token
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Creating a Connection
|
|
43
|
+
|
|
44
|
+
1. Go to Setup > Developer > Connections
|
|
45
|
+
2. Select service (Zoho CRM, Books, etc.) or Custom OAuth
|
|
46
|
+
3. Define required scopes
|
|
47
|
+
4. Click "Create and Connect"
|
|
48
|
+
5. Authorize in popup
|
|
49
|
+
6. Use connection name in `invokeUrl`
|
|
50
|
+
|
|
51
|
+
### Connection Scopes (Minimum Necessary)
|
|
52
|
+
|
|
53
|
+
| Zoho App | Common Scopes | When Needed |
|
|
54
|
+
|----------|---------------|-------------|
|
|
55
|
+
| CRM | `ZohoCRM.modules.ALL` | Full module access |
|
|
56
|
+
| CRM | `ZohoCRM.modules.READ` | Read-only reporting |
|
|
57
|
+
| Books | `ZohoBooks.fullaccess.all` | Full Books access |
|
|
58
|
+
| Books | `ZohoBooks.invoices.READ` | Invoice queries only |
|
|
59
|
+
| Creator | `ZohoCreator.form.CREATE` | Form submissions |
|
|
60
|
+
| Analytics | `ZohoAnalytics.fullaccess.all` | Reports and data |
|
|
61
|
+
|
|
62
|
+
> **WARNING:** Use the minimum scopes needed. Over-scoping creates security risk. For SOC2 clients, document scope justification.
|
|
63
|
+
|
|
64
|
+
### When Connections Fail
|
|
65
|
+
|
|
66
|
+
```deluge
|
|
67
|
+
try
|
|
68
|
+
{
|
|
69
|
+
response = invokeUrl [
|
|
70
|
+
url: "https://www.zohoapis.com/crm/v5/Deals"
|
|
71
|
+
type: GET
|
|
72
|
+
connection: "zoho-crm-connection"
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
catch (e)
|
|
76
|
+
{
|
|
77
|
+
errorStr = e.toString();
|
|
78
|
+
|
|
79
|
+
if (errorStr.contains("INVALID_TOKEN") || errorStr.contains("INVALID_OAUTH"))
|
|
80
|
+
{
|
|
81
|
+
// Connection needs reauthorization
|
|
82
|
+
sendmail [
|
|
83
|
+
from: zoho.adminuserid
|
|
84
|
+
to: "admin@company.com"
|
|
85
|
+
subject: "[URGENT] Zoho Connection Expired - Reauthorize Required"
|
|
86
|
+
message: "Connection 'zoho-crm-connection' needs reauthorization. Go to Setup > Connections to fix."
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
else
|
|
90
|
+
{
|
|
91
|
+
info "API error (non-auth): " + errorStr;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Manual Refresh Token Flow (Standalone Apps)
|
|
97
|
+
|
|
98
|
+
For Node.js, Python, or other standalone applications outside Zoho:
|
|
99
|
+
|
|
100
|
+
### Step 1: Initial Authorization
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
GET https://accounts.zoho.com/oauth/v2/auth?
|
|
104
|
+
scope=ZohoCRM.modules.ALL&
|
|
105
|
+
client_id=YOUR_CLIENT_ID&
|
|
106
|
+
response_type=code&
|
|
107
|
+
access_type=offline&
|
|
108
|
+
redirect_uri=https://your-app.com/callback&
|
|
109
|
+
prompt=consent
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Step 2: Exchange Code for Tokens
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
const axios = require("axios");
|
|
116
|
+
|
|
117
|
+
async function exchangeCodeForTokens(authCode) {
|
|
118
|
+
const response = await axios.post("https://accounts.zoho.com/oauth/v2/token", null, {
|
|
119
|
+
params: {
|
|
120
|
+
grant_type: "authorization_code",
|
|
121
|
+
client_id: process.env.ZOHO_CLIENT_ID,
|
|
122
|
+
client_secret: process.env.ZOHO_CLIENT_SECRET,
|
|
123
|
+
redirect_uri: process.env.ZOHO_REDIRECT_URI,
|
|
124
|
+
code: authCode
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Response contains:
|
|
129
|
+
// { access_token, refresh_token, expires_in: 3600, token_type: "Bearer" }
|
|
130
|
+
return response.data;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Step 3: Refresh Token (Automated)
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
class ZohoTokenManager {
|
|
138
|
+
constructor(clientId, clientSecret, refreshToken, dcDomain = "com") {
|
|
139
|
+
this.clientId = clientId;
|
|
140
|
+
this.clientSecret = clientSecret;
|
|
141
|
+
this.refreshToken = refreshToken;
|
|
142
|
+
this.accountsUrl = `https://accounts.zoho.${dcDomain}/oauth/v2/token`;
|
|
143
|
+
this.accessToken = null;
|
|
144
|
+
this.expiresAt = 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getAccessToken() {
|
|
148
|
+
// Refresh proactively at 50 minutes (before 60-minute expiry)
|
|
149
|
+
const bufferMs = 10 * 60 * 1000; // 10-minute buffer
|
|
150
|
+
if (this.accessToken && Date.now() < this.expiresAt - bufferMs) {
|
|
151
|
+
return this.accessToken;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const response = await axios.post(this.accountsUrl, null, {
|
|
156
|
+
params: {
|
|
157
|
+
refresh_token: this.refreshToken,
|
|
158
|
+
client_id: this.clientId,
|
|
159
|
+
client_secret: this.clientSecret,
|
|
160
|
+
grant_type: "refresh_token"
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (response.data.error) {
|
|
165
|
+
throw new Error(`Token refresh failed: ${response.data.error}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.accessToken = response.data.access_token;
|
|
169
|
+
this.expiresAt = Date.now() + (response.data.expires_in * 1000);
|
|
170
|
+
|
|
171
|
+
return this.accessToken;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error("Token refresh error:", error.message);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Make authenticated API call
|
|
179
|
+
async apiCall(url, method = "GET", data = null) {
|
|
180
|
+
const token = await this.getAccessToken();
|
|
181
|
+
|
|
182
|
+
const config = {
|
|
183
|
+
method,
|
|
184
|
+
url,
|
|
185
|
+
headers: {
|
|
186
|
+
"Authorization": `Zoho-oauthtoken ${token}`,
|
|
187
|
+
"Content-Type": "application/json"
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (data) config.data = data;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
return await axios(config);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
// If 401, force token refresh and retry once
|
|
197
|
+
if (error.response && error.response.status === 401) {
|
|
198
|
+
this.accessToken = null; // Force refresh
|
|
199
|
+
const newToken = await this.getAccessToken();
|
|
200
|
+
config.headers["Authorization"] = `Zoho-oauthtoken ${newToken}`;
|
|
201
|
+
return await axios(config);
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Usage
|
|
209
|
+
const zoho = new ZohoTokenManager(
|
|
210
|
+
process.env.ZOHO_CLIENT_ID,
|
|
211
|
+
process.env.ZOHO_CLIENT_SECRET,
|
|
212
|
+
process.env.ZOHO_REFRESH_TOKEN,
|
|
213
|
+
process.env.ZOHO_DC || "com"
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const deals = await zoho.apiCall("https://www.zohoapis.com/crm/v5/Deals");
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Token Storage Best Practices
|
|
220
|
+
|
|
221
|
+
### Where to Store Tokens
|
|
222
|
+
|
|
223
|
+
| Storage | Appropriate For | Security Level |
|
|
224
|
+
|---------|----------------|----------------|
|
|
225
|
+
| Catalyst Segments | Catalyst functions | Encrypted at rest |
|
|
226
|
+
| Environment variables | Server-side apps | Process-level isolation |
|
|
227
|
+
| Secret Manager (GCP/AWS) | Production systems | Full audit trail |
|
|
228
|
+
| .env file (local) | Development only | Gitignored |
|
|
229
|
+
|
|
230
|
+
### Where NEVER to Store Tokens
|
|
231
|
+
|
|
232
|
+
- Deluge variables or Creator fields
|
|
233
|
+
- Git repositories (even private)
|
|
234
|
+
- Client-side JavaScript
|
|
235
|
+
- Browser localStorage/cookies
|
|
236
|
+
- Log files
|
|
237
|
+
- Email or chat messages
|
|
238
|
+
- Hard-coded in source code
|
|
239
|
+
|
|
240
|
+
### Catalyst Segments Storage
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
// Store token in Catalyst Segments (encrypted key-value store)
|
|
244
|
+
const catalyst = require("zcatalyst-sdk-node");
|
|
245
|
+
|
|
246
|
+
async function storeToken(context, key, tokenData) {
|
|
247
|
+
const app = catalyst.initialize(context);
|
|
248
|
+
const segment = app.cache().segment();
|
|
249
|
+
|
|
250
|
+
await segment.put(key, JSON.stringify({
|
|
251
|
+
access_token: tokenData.access_token,
|
|
252
|
+
expires_at: Date.now() + (tokenData.expires_in * 1000),
|
|
253
|
+
stored_at: new Date().toISOString()
|
|
254
|
+
}), tokenData.expires_in); // TTL matches token expiry
|
|
255
|
+
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function getToken(context, key) {
|
|
260
|
+
const app = catalyst.initialize(context);
|
|
261
|
+
const segment = app.cache().segment();
|
|
262
|
+
|
|
263
|
+
const cached = await segment.get(key);
|
|
264
|
+
if (cached) {
|
|
265
|
+
const data = JSON.parse(cached.cache_value);
|
|
266
|
+
if (Date.now() < data.expires_at) {
|
|
267
|
+
return data.access_token;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return null; // Token expired or not found
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Multi-DC Token Management
|
|
275
|
+
|
|
276
|
+
Zoho operates in multiple data centers. Tokens are DC-specific:
|
|
277
|
+
|
|
278
|
+
| DC | Accounts URL | API Domain | Region |
|
|
279
|
+
|----|-------------|------------|--------|
|
|
280
|
+
| com | accounts.zoho.com | zohoapis.com | US |
|
|
281
|
+
| eu | accounts.zoho.eu | zohoapis.eu | Europe |
|
|
282
|
+
| in | accounts.zoho.in | zohoapis.in | India |
|
|
283
|
+
| com.au | accounts.zoho.com.au | zohoapis.com.au | Australia |
|
|
284
|
+
| jp | accounts.zoho.jp | zohoapis.jp | Japan |
|
|
285
|
+
| com.cn | accounts.zoho.com.cn | zohoapis.com.cn | China |
|
|
286
|
+
| ca | accounts.zohocloud.ca | zohoapis.ca | Canada |
|
|
287
|
+
|
|
288
|
+
### Multi-DC Implementation
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
// Determine correct DC based on client configuration
|
|
292
|
+
function getZohoUrls(dc) {
|
|
293
|
+
const dcMap = {
|
|
294
|
+
"com": { accounts: "accounts.zoho.com", api: "zohoapis.com" },
|
|
295
|
+
"eu": { accounts: "accounts.zoho.eu", api: "zohoapis.eu" },
|
|
296
|
+
"in": { accounts: "accounts.zoho.in", api: "zohoapis.in" },
|
|
297
|
+
"com.au": { accounts: "accounts.zoho.com.au", api: "zohoapis.com.au" },
|
|
298
|
+
"jp": { accounts: "accounts.zoho.jp", api: "zohoapis.jp" },
|
|
299
|
+
"ca": { accounts: "accounts.zohocloud.ca", api: "zohoapis.ca" }
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (!dcMap[dc]) {
|
|
303
|
+
throw new Error(`Unknown Zoho DC: ${dc}. Valid: ${Object.keys(dcMap).join(", ")}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
tokenUrl: `https://${dcMap[dc].accounts}/oauth/v2/token`,
|
|
308
|
+
authUrl: `https://${dcMap[dc].accounts}/oauth/v2/auth`,
|
|
309
|
+
apiBase: `https://www.${dcMap[dc].api}`
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Per-client token manager
|
|
314
|
+
class MultiDCTokenManager {
|
|
315
|
+
constructor(clientConfigs) {
|
|
316
|
+
// clientConfigs: { clientId: { dc, clientId, clientSecret, refreshToken } }
|
|
317
|
+
this.managers = {};
|
|
318
|
+
|
|
319
|
+
for (const [id, config] of Object.entries(clientConfigs)) {
|
|
320
|
+
this.managers[id] = new ZohoTokenManager(
|
|
321
|
+
config.clientId,
|
|
322
|
+
config.clientSecret,
|
|
323
|
+
config.refreshToken,
|
|
324
|
+
config.dc
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async getClientToken(clientId) {
|
|
330
|
+
if (!this.managers[clientId]) {
|
|
331
|
+
throw new Error(`No token manager for client: ${clientId}`);
|
|
332
|
+
}
|
|
333
|
+
return await this.managers[clientId].getAccessToken();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Error Handling for Expired Tokens
|
|
339
|
+
|
|
340
|
+
### Common Token Errors
|
|
341
|
+
|
|
342
|
+
| Error Code | Meaning | Action |
|
|
343
|
+
|-----------|---------|--------|
|
|
344
|
+
| INVALID_TOKEN | Access token expired | Refresh using refresh token |
|
|
345
|
+
| INVALID_OAUTH_TOKEN | Token malformed | Check token storage, regenerate |
|
|
346
|
+
| OAUTH_SCOPE_MISMATCH | Insufficient scope | Re-authorize with correct scopes |
|
|
347
|
+
| INVALID_CLIENT | Client ID/Secret wrong | Verify API console credentials |
|
|
348
|
+
| INVALID_CODE | Auth code expired (>10 min) | Restart auth flow |
|
|
349
|
+
| ACCESS_DENIED | User revoked access | Re-authorize, user must grant again |
|
|
350
|
+
|
|
351
|
+
### Handling Token Errors in Production
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
async function handleTokenError(error, clientId) {
|
|
355
|
+
const errorCode = error.response?.data?.code || error.message;
|
|
356
|
+
|
|
357
|
+
switch (errorCode) {
|
|
358
|
+
case "INVALID_TOKEN":
|
|
359
|
+
// Normal expiry - auto-refresh handles this
|
|
360
|
+
console.log("Token expired, refreshing...");
|
|
361
|
+
return await tokenManager.forceRefresh(clientId);
|
|
362
|
+
|
|
363
|
+
case "INVALID_OAUTH_TOKEN":
|
|
364
|
+
case "INVALID_CLIENT":
|
|
365
|
+
// Credentials issue - needs human intervention
|
|
366
|
+
await notifyAdmin({
|
|
367
|
+
subject: `[CRITICAL] OAuth credentials invalid for ${clientId}`,
|
|
368
|
+
message: `Error: ${errorCode}. Manual reauthorization required.`
|
|
369
|
+
});
|
|
370
|
+
throw new Error("OAuth credentials invalid - manual fix required");
|
|
371
|
+
|
|
372
|
+
case "OAUTH_SCOPE_MISMATCH":
|
|
373
|
+
// Scope changed - need new authorization
|
|
374
|
+
await notifyAdmin({
|
|
375
|
+
subject: `[ACTION] OAuth scope change needed for ${clientId}`,
|
|
376
|
+
message: `Current scopes insufficient. Re-authorize with updated scopes.`
|
|
377
|
+
});
|
|
378
|
+
throw new Error("Insufficient OAuth scopes");
|
|
379
|
+
|
|
380
|
+
case "ACCESS_DENIED":
|
|
381
|
+
// User revoked - completely broken until re-authorized
|
|
382
|
+
await notifyAdmin({
|
|
383
|
+
subject: `[CRITICAL] OAuth access revoked for ${clientId}`,
|
|
384
|
+
message: `User has revoked access. All API calls will fail until re-authorized.`
|
|
385
|
+
});
|
|
386
|
+
throw new Error("OAuth access revoked by user");
|
|
387
|
+
|
|
388
|
+
default:
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Multi-Org Token Management (Consultancy Pattern)
|
|
395
|
+
|
|
396
|
+
For CloudStream with multiple client orgs:
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
// .env.client file structure (gitignored)
|
|
400
|
+
// ZOHO_DC=com
|
|
401
|
+
// ZOHO_CLIENT_ID=1000.ABC123...
|
|
402
|
+
// ZOHO_CLIENT_SECRET=xyz789...
|
|
403
|
+
// ZOHO_REFRESH_TOKEN=1000.refresh...
|
|
404
|
+
// ZOHO_ORG_ID=12345678
|
|
405
|
+
|
|
406
|
+
// Client switch loads correct credentials
|
|
407
|
+
// Use /client-switch command to swap active client context
|
|
408
|
+
|
|
409
|
+
// Token isolation: Each client has completely separate OAuth app
|
|
410
|
+
// Never share refresh tokens between clients
|
|
411
|
+
// Store per-client in separate env files or secret manager keys
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Token Rotation Schedule
|
|
415
|
+
|
|
416
|
+
For SOC2/HIPAA compliance:
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
Monthly:
|
|
420
|
+
1. Generate new client_secret in Zoho API Console
|
|
421
|
+
2. Update all applications using that secret
|
|
422
|
+
3. Verify token refresh works with new secret
|
|
423
|
+
4. Revoke old secret after 24-hour verification period
|
|
424
|
+
5. Document rotation in change log
|
|
425
|
+
|
|
426
|
+
On Incident:
|
|
427
|
+
1. Immediately revoke compromised refresh token
|
|
428
|
+
2. Generate new OAuth application if client_secret exposed
|
|
429
|
+
3. Re-authorize all connections
|
|
430
|
+
4. Audit access logs for unauthorized use
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Testing Token Management
|
|
434
|
+
|
|
435
|
+
```javascript
|
|
436
|
+
// Verify token management is working correctly
|
|
437
|
+
async function tokenHealthCheck(clientId) {
|
|
438
|
+
try {
|
|
439
|
+
const token = await tokenManager.getAccessToken(clientId);
|
|
440
|
+
|
|
441
|
+
// Test the token with a simple API call
|
|
442
|
+
const response = await axios.get(
|
|
443
|
+
`${getZohoUrls(clientDC).apiBase}/crm/v5/org`,
|
|
444
|
+
{ headers: { "Authorization": `Zoho-oauthtoken ${token}` } }
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
status: "healthy",
|
|
449
|
+
org_name: response.data.org[0].company_name,
|
|
450
|
+
token_prefix: token.substring(0, 10) + "...",
|
|
451
|
+
dc: clientDC
|
|
452
|
+
};
|
|
453
|
+
} catch (error) {
|
|
454
|
+
return {
|
|
455
|
+
status: "unhealthy",
|
|
456
|
+
error: error.message,
|
|
457
|
+
action_required: true
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|