matimo-examples 0.1.0-alpha.12 → 0.1.0-alpha.12.1
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/credentials/credentials-example.ts +225 -0
- package/package.json +12 -10
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* PER-EXECUTION CREDENTIAL OVERRIDE — MULTI-TENANT EXAMPLE
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* PATTERN: Per-call credentials (options.credentials)
|
|
8
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
* Demonstrates how to supply credentials per `execute()` call instead of
|
|
10
|
+
* relying on environment variables. This is the right pattern for
|
|
11
|
+
* multi-tenant platforms where each user/tenant has their own API keys.
|
|
12
|
+
*
|
|
13
|
+
* Use this pattern when:
|
|
14
|
+
* ✅ Serving multiple tenants from a single process
|
|
15
|
+
* ✅ Credentials come from a database / secrets manager / vault
|
|
16
|
+
* ✅ You must NOT store per-tenant tokens in process.env
|
|
17
|
+
* ✅ You want strict per-call credential isolation
|
|
18
|
+
*
|
|
19
|
+
* Contrast with single-tenant pattern (env vars):
|
|
20
|
+
* SLACK_BOT_TOKEN=xoxb-xxx matimo execute slack-send-message ...
|
|
21
|
+
* → works fine for one account, breaks for ten tenants
|
|
22
|
+
*
|
|
23
|
+
* SETUP:
|
|
24
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
25
|
+
* No .env token needed — this example uses placeholder tenant tokens to
|
|
26
|
+
* show the API shape. Real requests will fail (expected). To see real calls
|
|
27
|
+
* succeed, replace the placeholder tokens with real Slack bot tokens.
|
|
28
|
+
*
|
|
29
|
+
* USAGE:
|
|
30
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
31
|
+
* pnpm credentials:example
|
|
32
|
+
*
|
|
33
|
+
* KEY CONCEPTS DEMONSTRATED:
|
|
34
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
35
|
+
* 1. getRequiredCredentials(toolName) — discover what keys a tool needs
|
|
36
|
+
* 2. execute(name, params, { credentials }) — per-call credential injection
|
|
37
|
+
* 3. Tenant isolation — two tenants, same process, different tokens
|
|
38
|
+
* 4. Graceful partial credentials — credential + env-var fallback strategy
|
|
39
|
+
* 5. Credential manifest — build a map of all tools → required keys at startup
|
|
40
|
+
*
|
|
41
|
+
* ============================================================================
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import 'dotenv/config';
|
|
45
|
+
import { MatimoInstance } from 'matimo';
|
|
46
|
+
|
|
47
|
+
// ─── Simulated tenant "database" ─────────────────────────────────────────────
|
|
48
|
+
// In a real platform these would come from your DB / vault / secrets manager.
|
|
49
|
+
const TENANTS = {
|
|
50
|
+
'tenant-acme': {
|
|
51
|
+
name: 'Acme Corp',
|
|
52
|
+
secrets: {
|
|
53
|
+
// Replace with a real token to see live Slack calls
|
|
54
|
+
SLACK_BOT_TOKEN: process.env.ACME_SLACK_BOT_TOKEN ?? 'xoxb-acme-placeholder-token',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
'tenant-globex': {
|
|
58
|
+
name: 'Globex Inc',
|
|
59
|
+
secrets: {
|
|
60
|
+
SLACK_BOT_TOKEN: process.env.GLOBEX_SLACK_BOT_TOKEN ?? 'xoxb-globex-placeholder-token',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type TenantId = keyof typeof TENANTS;
|
|
66
|
+
type Tenant = (typeof TENANTS)[TenantId];
|
|
67
|
+
|
|
68
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
console.info('\n╔════════════════════════════════════════════════════════╗');
|
|
72
|
+
console.info('║ Per-Execution Credential Override — Multi-Tenant ║');
|
|
73
|
+
console.info('╚════════════════════════════════════════════════════════╝\n');
|
|
74
|
+
|
|
75
|
+
// ── 1. Initialize once — no per-tenant init needed ──────────────────────
|
|
76
|
+
console.info('🚀 Initializing Matimo (once for all tenants)…');
|
|
77
|
+
const matimo = await MatimoInstance.init({ autoDiscover: true });
|
|
78
|
+
console.info(`✅ Loaded ${matimo.listTools().length} tools\n`);
|
|
79
|
+
|
|
80
|
+
// ── 2. Discover required credential keys at startup ──────────────────────
|
|
81
|
+
// getRequiredCredentials() tells you EXACTLY what keys to put in `credentials`
|
|
82
|
+
// for a given tool — no need to read the YAML.
|
|
83
|
+
console.info('🔑 Building credential manifest for all tools…');
|
|
84
|
+
const credentialManifest: Record<string, string[]> = {};
|
|
85
|
+
for (const tool of matimo.listTools()) {
|
|
86
|
+
const keys = matimo.getRequiredCredentials(tool.name);
|
|
87
|
+
if (keys.length > 0) {
|
|
88
|
+
credentialManifest[tool.name] = keys;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const toolsWithAuth = Object.keys(credentialManifest).length;
|
|
93
|
+
const toolsNoAuth = matimo.listTools().length - toolsWithAuth;
|
|
94
|
+
console.info(` ${toolsWithAuth} tools need credentials, ${toolsNoAuth} are public`);
|
|
95
|
+
console.info(' Sample manifest entries:');
|
|
96
|
+
for (const [tool, keys] of Object.entries(credentialManifest).slice(0, 5)) {
|
|
97
|
+
console.info(` ${tool}: [${keys.join(', ')}]`);
|
|
98
|
+
}
|
|
99
|
+
console.info();
|
|
100
|
+
|
|
101
|
+
// ── 3. Per-tenant execution helper ───────────────────────────────────────
|
|
102
|
+
// Collect only the keys the tool needs from the tenant's secrets store.
|
|
103
|
+
async function executeForTenant(
|
|
104
|
+
tenantId: TenantId,
|
|
105
|
+
toolName: string,
|
|
106
|
+
params: Record<string, unknown>
|
|
107
|
+
) {
|
|
108
|
+
const tenant: Tenant = TENANTS[tenantId];
|
|
109
|
+
const requiredKeys = matimo.getRequiredCredentials(toolName);
|
|
110
|
+
|
|
111
|
+
// Build credentials map — only the keys this specific tool needs
|
|
112
|
+
const credentials: Record<string, string> = {};
|
|
113
|
+
const missing: string[] = [];
|
|
114
|
+
|
|
115
|
+
for (const key of requiredKeys) {
|
|
116
|
+
const value = tenant.secrets[key as keyof typeof tenant.secrets];
|
|
117
|
+
if (value) {
|
|
118
|
+
credentials[key] = value;
|
|
119
|
+
} else {
|
|
120
|
+
missing.push(key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (missing.length > 0) {
|
|
125
|
+
console.warn(
|
|
126
|
+
` ⚠️ [${tenant.name}] Missing ${missing.length} credential key(s) for '${toolName}'.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.info(
|
|
131
|
+
` 🏢 [${tenant.name}] Executing '${toolName}' with ${Object.keys(credentials).length} credential(s)…`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const result = await matimo.execute(toolName, params, { credentials });
|
|
136
|
+
return { tenantId, toolName, success: true, result };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
// Expected for placeholder tokens — real tokens would succeed
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
140
|
+
return { tenantId, toolName, success: false, error: message };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── 4. Demo: same tool, two tenants, fully isolated ──────────────────────
|
|
145
|
+
console.info('════════════════════════════════════════════════════════════');
|
|
146
|
+
console.info('Demo 1: Same tool, two tenants, isolated credentials');
|
|
147
|
+
console.info('════════════════════════════════════════════════════════════\n');
|
|
148
|
+
|
|
149
|
+
const channel = process.env.SLACK_CHANNEL_ID ?? 'C0000000000';
|
|
150
|
+
const params = { channel, text: `Hello from multi-tenant demo at ${new Date().toISOString()}` };
|
|
151
|
+
|
|
152
|
+
const [acmeResult, globexResult] = await Promise.all([
|
|
153
|
+
executeForTenant('tenant-acme', 'slack-send-message', params),
|
|
154
|
+
executeForTenant('tenant-globex', 'slack-send-message', params),
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
for (const r of [acmeResult, globexResult]) {
|
|
158
|
+
const icon = r.success ? '✅' : '⚠️ ';
|
|
159
|
+
const tenant = TENANTS[r.tenantId as TenantId].name;
|
|
160
|
+
console.info(
|
|
161
|
+
` ${icon} [${tenant}] ${r.success ? 'Succeeded' : `Failed (expected with placeholder token): ${r.error?.slice(0, 80)}`}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── 5. Verify process.env was NOT modified ────────────────────────────────
|
|
166
|
+
console.info('\n════════════════════════════════════════════════════════════');
|
|
167
|
+
console.info('Demo 2: process.env isolation check');
|
|
168
|
+
console.info('════════════════════════════════════════════════════════════\n');
|
|
169
|
+
|
|
170
|
+
const envBefore = process.env.SLACK_BOT_TOKEN;
|
|
171
|
+
await executeForTenant('tenant-acme', 'slack-send-message', params);
|
|
172
|
+
const envAfter = process.env.SLACK_BOT_TOKEN;
|
|
173
|
+
|
|
174
|
+
if (envBefore === envAfter) {
|
|
175
|
+
console.info(' ✅ process.env.SLACK_BOT_TOKEN unchanged — credentials are call-scoped');
|
|
176
|
+
} else {
|
|
177
|
+
console.error(' ❌ UNEXPECTED: process.env was mutated!');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── 6. Fallback to env vars when credentials not provided ─────────────────
|
|
181
|
+
console.info('\n════════════════════════════════════════════════════════════');
|
|
182
|
+
console.info('Demo 3: Backward compatibility — no credentials → env var fallback');
|
|
183
|
+
console.info('════════════════════════════════════════════════════════════\n');
|
|
184
|
+
|
|
185
|
+
// Temporarily set env var to simulate single-tenant / legacy usage
|
|
186
|
+
const wasSet = !!process.env.SLACK_BOT_TOKEN;
|
|
187
|
+
process.env.SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN ?? 'xoxb-env-fallback-token';
|
|
188
|
+
|
|
189
|
+
console.info(' Calling execute() without credentials — falls back to process.env…');
|
|
190
|
+
try {
|
|
191
|
+
await matimo.execute('slack-send-message', params);
|
|
192
|
+
console.info(' ✅ Succeeded (env var token was valid)');
|
|
193
|
+
} catch {
|
|
194
|
+
console.info(' ⚠️ Failed at API level (env token is a placeholder — expected)');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!wasSet) delete process.env.SLACK_BOT_TOKEN;
|
|
198
|
+
|
|
199
|
+
// ── 7. Credential key lookup reference ───────────────────────────────────
|
|
200
|
+
console.info('\n════════════════════════════════════════════════════════════');
|
|
201
|
+
console.info('Reference: credential keys for Slack tools');
|
|
202
|
+
console.info('════════════════════════════════════════════════════════════\n');
|
|
203
|
+
|
|
204
|
+
const slackTools = matimo.listTools().filter((t) => t.name.startsWith('slack'));
|
|
205
|
+
for (const tool of slackTools.slice(0, 5)) {
|
|
206
|
+
const keys = matimo.getRequiredCredentials(tool.name);
|
|
207
|
+
console.info(` ${tool.name}`);
|
|
208
|
+
console.info(
|
|
209
|
+
` credentials required: ${keys.length ? `${keys.length} key(s)` : '(none required)'}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (slackTools.length > 5) {
|
|
213
|
+
console.info(` … and ${slackTools.length - 5} more Slack tools`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.info('\n✅ Example complete.\n');
|
|
217
|
+
console.info('To use real credentials, set per-tenant env vars:');
|
|
218
|
+
console.info(' ACME_SLACK_BOT_TOKEN=xoxb-acme-real-token');
|
|
219
|
+
console.info(' GLOBEX_SLACK_BOT_TOKEN=xoxb-globex-real-token\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main().catch((err) => {
|
|
223
|
+
console.error('Fatal error:', err);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matimo-examples",
|
|
3
|
-
"version": "0.1.0-alpha.12",
|
|
3
|
+
"version": "0.1.0-alpha.12.1",
|
|
4
4
|
"description": "Matimo SDK examples - Factory Pattern, Decorator Pattern, and LangChain integration with Slack and Gmail",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
"langchain": "^1.2.16",
|
|
10
10
|
"dotenv": "^17.2.3",
|
|
11
11
|
"zod": "^4.3.6",
|
|
12
|
-
"matimo": "0.1.0-alpha.12",
|
|
13
|
-
"@matimo/
|
|
14
|
-
"@matimo/
|
|
15
|
-
"@matimo/gmail": "0.1.0-alpha.12",
|
|
16
|
-
"@matimo/github": "0.1.0-alpha.12",
|
|
17
|
-
"@matimo/
|
|
18
|
-
"@matimo/
|
|
19
|
-
"@matimo/
|
|
20
|
-
"@matimo/
|
|
12
|
+
"matimo": "0.1.0-alpha.12.1",
|
|
13
|
+
"@matimo/core": "0.1.0-alpha.12.1",
|
|
14
|
+
"@matimo/slack": "0.1.0-alpha.12.1",
|
|
15
|
+
"@matimo/gmail": "0.1.0-alpha.12.1",
|
|
16
|
+
"@matimo/github": "0.1.0-alpha.12.1",
|
|
17
|
+
"@matimo/postgres": "0.1.0-alpha.12.1",
|
|
18
|
+
"@matimo/twilio": "0.1.0-alpha.12.1",
|
|
19
|
+
"@matimo/hubspot": "0.1.0-alpha.12.1",
|
|
20
|
+
"@matimo/mailchimp": "0.1.0-alpha.12.1",
|
|
21
|
+
"@matimo/notion": "0.1.0-alpha.12.1"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@types/node": "^25.1.0",
|
|
@@ -73,6 +74,7 @@
|
|
|
73
74
|
"twilio:factory": "tsx twilio/twilio-factory.ts",
|
|
74
75
|
"twilio:decorator": "tsx twilio/twilio-decorator.ts",
|
|
75
76
|
"twilio:langchain": "tsx twilio/twilio-langchain.ts",
|
|
77
|
+
"credentials:example": "tsx credentials/credentials-example.ts",
|
|
76
78
|
"build": "tsc",
|
|
77
79
|
"clean": "rm -rf dist"
|
|
78
80
|
}
|