synthos 0.6.0 → 0.7.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 +33 -1
- package/default-pages/app_builder.html +40 -0
- package/default-pages/app_builder.json +1 -0
- package/default-pages/json_tools.html +89 -159
- package/default-pages/json_tools.json +1 -0
- package/default-pages/my_notes.html +33 -0
- package/default-pages/my_notes.json +12 -0
- package/default-pages/neon_asteroids.html +77 -0
- package/default-pages/neon_asteroids.json +12 -0
- package/default-pages/sidebar_builder.html +49 -0
- package/default-pages/sidebar_builder.json +1 -0
- package/default-pages/solar_explorer.html +1956 -0
- package/default-pages/solar_explorer.json +12 -0
- package/default-pages/solar_tutorial.html +476 -0
- package/default-pages/solar_tutorial.json +1 -0
- package/default-pages/two-panel_builder.html +66 -0
- package/default-pages/two-panel_builder.json +1 -0
- package/dist/connectors/index.d.ts +3 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +6 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/registry.d.ts +3 -0
- package/dist/connectors/registry.d.ts.map +1 -0
- package/dist/connectors/registry.js +100 -0
- package/dist/connectors/registry.js.map +1 -0
- package/dist/connectors/types.d.ts +61 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +3 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/files.d.ts +2 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +12 -1
- package/dist/files.js.map +1 -1
- package/dist/init.d.ts +8 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +155 -3
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts +11 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +281 -0
- package/dist/migrations.js.map +1 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +10 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/providers.d.ts +7 -0
- package/dist/models/providers.d.ts.map +1 -0
- package/dist/models/providers.js +33 -0
- package/dist/models/providers.js.map +1 -0
- package/dist/models/types.d.ts +21 -0
- package/dist/models/types.d.ts.map +1 -0
- package/dist/models/types.js +3 -0
- package/dist/models/types.js.map +1 -0
- package/dist/pages.d.ts +21 -2
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +202 -23
- package/dist/pages.js.map +1 -1
- package/dist/scripts.js +2 -2
- package/dist/scripts.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts +3 -2
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +11 -16
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/debugLog.d.ts +11 -0
- package/dist/service/debugLog.d.ts.map +1 -0
- package/dist/service/debugLog.js +26 -0
- package/dist/service/debugLog.js.map +1 -0
- package/dist/service/modelInstructions.d.ts +7 -0
- package/dist/service/modelInstructions.d.ts.map +1 -0
- package/dist/service/modelInstructions.js +16 -0
- package/dist/service/modelInstructions.js.map +1 -0
- package/dist/service/requiresSettings.d.ts +2 -2
- package/dist/service/requiresSettings.d.ts.map +1 -1
- package/dist/service/requiresSettings.js.map +1 -1
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +15 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +81 -2
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +672 -82
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +579 -13
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts +4 -0
- package/dist/service/useConnectorRoutes.d.ts.map +1 -0
- package/dist/service/useConnectorRoutes.js +389 -0
- package/dist/service/useConnectorRoutes.js.map +1 -0
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +83 -70
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +243 -38
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/settings.d.ts +33 -4
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +108 -15
- package/dist/settings.js.map +1 -1
- package/dist/synthos-cli.d.ts.map +1 -1
- package/dist/synthos-cli.js +11 -1
- package/dist/synthos-cli.js.map +1 -1
- package/dist/themes.d.ts +9 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +64 -0
- package/dist/themes.js.map +1 -0
- package/package.json +5 -3
- package/required-pages/builder.html +74 -0
- package/required-pages/builder.json +1 -0
- package/required-pages/pages.html +169 -126
- package/required-pages/pages.json +1 -0
- package/required-pages/settings.html +812 -156
- package/required-pages/settings.json +1 -0
- package/required-pages/synthos_apis.html +272 -0
- package/required-pages/synthos_apis.json +1 -0
- package/required-pages/synthos_scripts.html +87 -0
- package/required-pages/synthos_scripts.json +1 -0
- package/src/connectors/index.ts +12 -0
- package/src/connectors/registry.ts +98 -0
- package/src/connectors/types.ts +68 -0
- package/src/files.ts +11 -0
- package/src/init.ts +151 -5
- package/src/migrations.ts +266 -0
- package/src/models/index.ts +2 -0
- package/src/models/providers.ts +33 -0
- package/src/models/types.ts +23 -0
- package/src/pages.ts +234 -26
- package/src/scripts.ts +2 -2
- package/src/service/createCompletePrompt.ts +14 -18
- package/src/service/debugLog.ts +17 -0
- package/src/service/modelInstructions.ts +14 -0
- package/src/service/requiresSettings.ts +3 -3
- package/src/service/server.ts +19 -2
- package/src/service/transformPage.ts +709 -88
- package/src/service/useApiRoutes.ts +632 -16
- package/src/service/useConnectorRoutes.ts +427 -0
- package/src/service/useDataRoutes.ts +87 -71
- package/src/service/usePageRoutes.ts +237 -44
- package/src/settings.ts +143 -20
- package/src/synthos-cli.ts +11 -1
- package/src/themes.ts +71 -0
- package/default-pages/[application].html +0 -95
- package/default-pages/[markdown].html +0 -271
- package/default-pages/[sidebar].html +0 -114
- package/default-pages/[split-application].html +0 -118
- package/default-pages/solar_system.html +0 -432
- package/default-pages/space_invaders.html +0 -617
- package/required-pages/apis.html +0 -362
- package/required-pages/home.html +0 -126
- package/required-pages/scripts.html +0 -350
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { Application } from 'express';
|
|
2
|
+
import { SynthOSConfig } from '../init';
|
|
3
|
+
import { loadSettings, saveSettings } from '../settings';
|
|
4
|
+
import {
|
|
5
|
+
CONNECTOR_REGISTRY,
|
|
6
|
+
ConnectorSummary,
|
|
7
|
+
ConnectorDetail,
|
|
8
|
+
ConnectorCallRequest,
|
|
9
|
+
ConnectorOAuthConfig
|
|
10
|
+
} from '../connectors';
|
|
11
|
+
|
|
12
|
+
export function useConnectorRoutes(config: SynthOSConfig, app: Application): void {
|
|
13
|
+
|
|
14
|
+
// GET /api/connectors — List connectors (minimal summaries)
|
|
15
|
+
// Also handles POST /api/connectors — Proxy call (see below)
|
|
16
|
+
app.get('/api/connectors', async (req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
19
|
+
const connectors = settings.connectors ?? {};
|
|
20
|
+
|
|
21
|
+
const categoryFilter = req.query.category as string | undefined;
|
|
22
|
+
const idFilter = req.query.id as string | undefined;
|
|
23
|
+
|
|
24
|
+
const list: ConnectorSummary[] = CONNECTOR_REGISTRY
|
|
25
|
+
.filter(def => {
|
|
26
|
+
if (categoryFilter && def.category !== categoryFilter) return false;
|
|
27
|
+
if (idFilter && def.id !== idFilter) return false;
|
|
28
|
+
return true;
|
|
29
|
+
})
|
|
30
|
+
.map(def => {
|
|
31
|
+
const cfg = connectors[def.id];
|
|
32
|
+
const isOAuth = def.authStrategy === 'oauth2';
|
|
33
|
+
const oauthCfg = cfg as ConnectorOAuthConfig | undefined;
|
|
34
|
+
return {
|
|
35
|
+
id: def.id,
|
|
36
|
+
name: def.name,
|
|
37
|
+
category: def.category,
|
|
38
|
+
configured: isOAuth
|
|
39
|
+
? !!oauthCfg && oauthCfg.enabled && !!oauthCfg.accessToken
|
|
40
|
+
: !!cfg && cfg.enabled && !!cfg.apiKey
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
res.json(list);
|
|
45
|
+
} catch (err: unknown) {
|
|
46
|
+
console.error(err);
|
|
47
|
+
res.status(500).json({ error: (err as Error).message });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// GET /api/connectors/:id — Full connector detail for config modal
|
|
52
|
+
app.get('/api/connectors/:id', async (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const { id } = req.params;
|
|
55
|
+
const def = CONNECTOR_REGISTRY.find(d => d.id === id);
|
|
56
|
+
if (!def) {
|
|
57
|
+
res.status(404).json({ error: `Connector "${id}" not found` });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
62
|
+
const cfg = (settings.connectors ?? {})[id];
|
|
63
|
+
const isOAuth = def.authStrategy === 'oauth2';
|
|
64
|
+
const oauthCfg = cfg as ConnectorOAuthConfig | undefined;
|
|
65
|
+
|
|
66
|
+
const detail: ConnectorDetail = {
|
|
67
|
+
...def,
|
|
68
|
+
configured: isOAuth
|
|
69
|
+
? !!oauthCfg && oauthCfg.enabled && !!oauthCfg.accessToken
|
|
70
|
+
: !!cfg && cfg.enabled && !!cfg.apiKey,
|
|
71
|
+
enabled: !!cfg?.enabled,
|
|
72
|
+
hasKey: isOAuth ? !!oauthCfg?.clientId : !!cfg?.apiKey,
|
|
73
|
+
connected: isOAuth ? !!oauthCfg?.accessToken : undefined,
|
|
74
|
+
accountName: isOAuth ? oauthCfg?.accountName : undefined,
|
|
75
|
+
userId: isOAuth ? oauthCfg?.userId : undefined
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
res.json(detail);
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
console.error(err);
|
|
81
|
+
res.status(500).json({ error: (err as Error).message });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// POST /api/connectors/:id — Save connector config (API key + enabled toggle)
|
|
86
|
+
app.post('/api/connectors/:id', async (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const { id } = req.params;
|
|
89
|
+
const def = CONNECTOR_REGISTRY.find(d => d.id === id);
|
|
90
|
+
if (!def) {
|
|
91
|
+
res.status(404).json({ error: `Connector "${id}" not found` });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
96
|
+
const existing = settings.connectors ?? {};
|
|
97
|
+
|
|
98
|
+
if (def.authStrategy === 'oauth2') {
|
|
99
|
+
const { clientId, clientSecret, accessToken, userId, enabled } = req.body;
|
|
100
|
+
const prev = (existing[id] as ConnectorOAuthConfig) ?? { apiKey: '', enabled: false };
|
|
101
|
+
const entry: ConnectorOAuthConfig = {
|
|
102
|
+
...prev,
|
|
103
|
+
apiKey: prev.apiKey || '',
|
|
104
|
+
enabled: typeof enabled === 'boolean' ? enabled : prev.enabled
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// OAuth app credentials
|
|
108
|
+
if (typeof clientId === 'string' && clientId.length > 0) entry.clientId = clientId;
|
|
109
|
+
if (typeof clientSecret === 'string' && clientSecret.length > 0) entry.clientSecret = clientSecret;
|
|
110
|
+
|
|
111
|
+
// Manual token entry
|
|
112
|
+
if (typeof accessToken === 'string' && accessToken.length > 0) {
|
|
113
|
+
entry.accessToken = accessToken;
|
|
114
|
+
entry.apiKey = accessToken;
|
|
115
|
+
}
|
|
116
|
+
if (typeof userId === 'string' && userId.length > 0) {
|
|
117
|
+
entry.userId = userId;
|
|
118
|
+
entry.accountName = userId;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const updated = { ...existing, [id]: entry };
|
|
122
|
+
await saveSettings(config.pagesFolder, { connectors: updated });
|
|
123
|
+
} else {
|
|
124
|
+
const { apiKey, enabled } = req.body;
|
|
125
|
+
const resolvedKey = (typeof apiKey === 'string' && apiKey.length > 0)
|
|
126
|
+
? apiKey
|
|
127
|
+
: (existing[id]?.apiKey ?? '');
|
|
128
|
+
|
|
129
|
+
const updated = {
|
|
130
|
+
...existing,
|
|
131
|
+
[id]: { apiKey: resolvedKey, enabled: !!enabled }
|
|
132
|
+
};
|
|
133
|
+
await saveSettings(config.pagesFolder, { connectors: updated });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
res.json({ saved: true });
|
|
137
|
+
} catch (err: unknown) {
|
|
138
|
+
console.error(err);
|
|
139
|
+
res.status(500).json({ error: (err as Error).message });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// DELETE /api/connectors/:id — Remove connector config
|
|
144
|
+
app.delete('/api/connectors/:id', async (req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const { id } = req.params;
|
|
147
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
148
|
+
const existing = settings.connectors ?? {};
|
|
149
|
+
|
|
150
|
+
const updated = { ...existing };
|
|
151
|
+
delete updated[id];
|
|
152
|
+
|
|
153
|
+
await saveSettings(config.pagesFolder, { connectors: updated });
|
|
154
|
+
res.json({ deleted: true });
|
|
155
|
+
} catch (err: unknown) {
|
|
156
|
+
console.error(err);
|
|
157
|
+
res.status(500).json({ error: (err as Error).message });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// GET /api/connectors/:id/authorize — Start OAuth2 flow
|
|
162
|
+
app.get('/api/connectors/:id/authorize', async (req, res) => {
|
|
163
|
+
try {
|
|
164
|
+
const { id } = req.params;
|
|
165
|
+
const def = CONNECTOR_REGISTRY.find(d => d.id === id);
|
|
166
|
+
if (!def || def.authStrategy !== 'oauth2') {
|
|
167
|
+
res.status(400).json({ error: `Connector "${id}" is not an OAuth2 connector` });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
172
|
+
const cfg = (settings.connectors ?? {})[id] as ConnectorOAuthConfig | undefined;
|
|
173
|
+
if (!cfg?.clientId || !cfg?.clientSecret) {
|
|
174
|
+
res.status(400).json({ error: 'Client ID and Client Secret must be saved before authorizing' });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const redirectUri = `${req.protocol}://${req.get('host')}/api/connectors/callback`;
|
|
179
|
+
const state = JSON.stringify({ connector: id });
|
|
180
|
+
|
|
181
|
+
const authUrl = new URL(def.authorizationUrl!);
|
|
182
|
+
authUrl.searchParams.set('client_id', cfg.clientId);
|
|
183
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
184
|
+
authUrl.searchParams.set('scope', (def.scopes ?? []).join(','));
|
|
185
|
+
authUrl.searchParams.set('state', state);
|
|
186
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
187
|
+
|
|
188
|
+
res.redirect(authUrl.toString());
|
|
189
|
+
} catch (err: unknown) {
|
|
190
|
+
console.error(err);
|
|
191
|
+
res.status(500).json({ error: (err as Error).message });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// GET /api/connectors/callback — OAuth2 callback
|
|
196
|
+
app.get('/api/connectors/callback', async (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
const code = req.query.code as string | undefined;
|
|
199
|
+
const stateRaw = req.query.state as string | undefined;
|
|
200
|
+
const error = req.query.error as string | undefined;
|
|
201
|
+
|
|
202
|
+
if (error) {
|
|
203
|
+
res.redirect(`/settings?tab=connectors&error=${encodeURIComponent(error)}`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!code || !stateRaw) {
|
|
208
|
+
res.status(400).json({ error: 'Missing code or state parameter' });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const state = JSON.parse(stateRaw) as { connector: string };
|
|
213
|
+
const connectorId = state.connector;
|
|
214
|
+
|
|
215
|
+
const def = CONNECTOR_REGISTRY.find(d => d.id === connectorId);
|
|
216
|
+
if (!def || def.authStrategy !== 'oauth2') {
|
|
217
|
+
res.status(400).json({ error: `Unknown OAuth2 connector: ${connectorId}` });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
222
|
+
const cfg = (settings.connectors ?? {})[connectorId] as ConnectorOAuthConfig | undefined;
|
|
223
|
+
if (!cfg?.clientId || !cfg?.clientSecret) {
|
|
224
|
+
res.status(400).json({ error: 'Client credentials not found' });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const redirectUri = `${req.protocol}://${req.get('host')}/api/connectors/callback`;
|
|
229
|
+
|
|
230
|
+
// Step 1: Exchange code for short-lived token
|
|
231
|
+
const tokenUrl = new URL(def.tokenUrl!);
|
|
232
|
+
tokenUrl.searchParams.set('client_id', cfg.clientId);
|
|
233
|
+
tokenUrl.searchParams.set('client_secret', cfg.clientSecret);
|
|
234
|
+
tokenUrl.searchParams.set('redirect_uri', redirectUri);
|
|
235
|
+
tokenUrl.searchParams.set('code', code);
|
|
236
|
+
tokenUrl.searchParams.set('grant_type', 'authorization_code');
|
|
237
|
+
|
|
238
|
+
const tokenRes = await fetch(tokenUrl.toString());
|
|
239
|
+
if (!tokenRes.ok) {
|
|
240
|
+
const text = await tokenRes.text();
|
|
241
|
+
console.error('Token exchange failed:', text);
|
|
242
|
+
res.redirect(`/settings?tab=connectors&error=${encodeURIComponent('Token exchange failed')}`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const tokenData = await tokenRes.json() as { access_token: string; token_type?: string; expires_in?: number };
|
|
246
|
+
let accessToken = tokenData.access_token;
|
|
247
|
+
let expiresAt = tokenData.expires_in ? Date.now() + tokenData.expires_in * 1000 : 0;
|
|
248
|
+
|
|
249
|
+
// Step 2: For Instagram — exchange for long-lived token
|
|
250
|
+
if (connectorId === 'instagram') {
|
|
251
|
+
const llUrl = new URL('https://graph.facebook.com/v21.0/oauth/access_token');
|
|
252
|
+
llUrl.searchParams.set('grant_type', 'fb_exchange_token');
|
|
253
|
+
llUrl.searchParams.set('client_id', cfg.clientId);
|
|
254
|
+
llUrl.searchParams.set('client_secret', cfg.clientSecret);
|
|
255
|
+
llUrl.searchParams.set('fb_exchange_token', accessToken);
|
|
256
|
+
|
|
257
|
+
const llRes = await fetch(llUrl.toString());
|
|
258
|
+
if (llRes.ok) {
|
|
259
|
+
const llData = await llRes.json() as { access_token: string; expires_in?: number };
|
|
260
|
+
accessToken = llData.access_token;
|
|
261
|
+
expiresAt = llData.expires_in ? Date.now() + llData.expires_in * 1000 : 0;
|
|
262
|
+
} else {
|
|
263
|
+
console.error('Long-lived token exchange failed:', await llRes.text());
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Step 3: For Instagram — resolve IG Business Account ID
|
|
268
|
+
let userId = '';
|
|
269
|
+
let accountName = '';
|
|
270
|
+
if (connectorId === 'instagram') {
|
|
271
|
+
try {
|
|
272
|
+
const pagesRes = await fetch(`https://graph.facebook.com/v21.0/me/accounts?access_token=${encodeURIComponent(accessToken)}`);
|
|
273
|
+
if (pagesRes.ok) {
|
|
274
|
+
const pagesData = await pagesRes.json() as { data: Array<{ id: string; name: string }> };
|
|
275
|
+
if (pagesData.data && pagesData.data.length > 0) {
|
|
276
|
+
const page = pagesData.data[0];
|
|
277
|
+
accountName = page.name;
|
|
278
|
+
|
|
279
|
+
const igRes = await fetch(`https://graph.facebook.com/v21.0/${page.id}?fields=instagram_business_account&access_token=${encodeURIComponent(accessToken)}`);
|
|
280
|
+
if (igRes.ok) {
|
|
281
|
+
const igData = await igRes.json() as { instagram_business_account?: { id: string } };
|
|
282
|
+
if (igData.instagram_business_account) {
|
|
283
|
+
userId = igData.instagram_business_account.id;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} catch (igErr) {
|
|
289
|
+
console.error('Failed to resolve Instagram Business Account:', igErr);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Step 4: Save tokens to settings
|
|
294
|
+
const existing = settings.connectors ?? {};
|
|
295
|
+
const prev = (existing[connectorId] as ConnectorOAuthConfig) ?? { apiKey: '', enabled: false };
|
|
296
|
+
const updated = {
|
|
297
|
+
...existing,
|
|
298
|
+
[connectorId]: {
|
|
299
|
+
...prev,
|
|
300
|
+
apiKey: prev.apiKey || accessToken,
|
|
301
|
+
accessToken,
|
|
302
|
+
expiresAt,
|
|
303
|
+
userId,
|
|
304
|
+
accountName,
|
|
305
|
+
enabled: true
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
await saveSettings(config.pagesFolder, { connectors: updated });
|
|
309
|
+
|
|
310
|
+
res.redirect(`/settings?tab=connectors&connected=${encodeURIComponent(connectorId)}`);
|
|
311
|
+
} catch (err: unknown) {
|
|
312
|
+
console.error('OAuth callback error:', err);
|
|
313
|
+
res.redirect(`/settings?tab=connectors&error=${encodeURIComponent((err as Error).message)}`);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// POST /api/connectors — Proxy call
|
|
318
|
+
app.post('/api/connectors', async (req, res) => {
|
|
319
|
+
try {
|
|
320
|
+
const request = req.body as ConnectorCallRequest;
|
|
321
|
+
|
|
322
|
+
if (!request.connector || !request.method || !request.path) {
|
|
323
|
+
res.status(400).json({ error: 'connector, method, and path are required' });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const def = CONNECTOR_REGISTRY.find(d => d.id === request.connector);
|
|
328
|
+
if (!def) {
|
|
329
|
+
res.status(404).json({ error: `Connector "${request.connector}" not found` });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const settings = await loadSettings(config.pagesFolder);
|
|
334
|
+
const cfg = (settings.connectors ?? {})[request.connector];
|
|
335
|
+
|
|
336
|
+
if (def.authStrategy === 'oauth2') {
|
|
337
|
+
const oauthCfg = cfg as ConnectorOAuthConfig | undefined;
|
|
338
|
+
if (!oauthCfg || !oauthCfg.enabled || !oauthCfg.accessToken) {
|
|
339
|
+
res.status(400).json({ error: `Connector "${request.connector}" is not configured or not enabled` });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Check token expiry
|
|
343
|
+
if (oauthCfg.expiresAt && oauthCfg.expiresAt < Date.now()) {
|
|
344
|
+
res.status(401).json({ error: `Access token for "${request.connector}" has expired. Please re-authorize in Settings > Connectors.` });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
if (!cfg || !cfg.enabled || !cfg.apiKey) {
|
|
349
|
+
res.status(400).json({ error: `Connector "${request.connector}" is not configured or not enabled` });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Build URL — join baseUrl path with request path to avoid
|
|
355
|
+
// absolute paths (e.g. "/me/accounts") replacing the base path
|
|
356
|
+
const base = new URL(def.baseUrl);
|
|
357
|
+
const joinedPath = base.pathname.replace(/\/+$/, '') + '/' + request.path.replace(/^\/+/, '');
|
|
358
|
+
base.pathname = joinedPath;
|
|
359
|
+
const url = base;
|
|
360
|
+
if (request.query) {
|
|
361
|
+
for (const [key, value] of Object.entries(request.query)) {
|
|
362
|
+
url.searchParams.set(key, value);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Attach auth
|
|
367
|
+
const headers: Record<string, string> = { ...request.headers };
|
|
368
|
+
switch (def.authStrategy) {
|
|
369
|
+
case 'bearer':
|
|
370
|
+
headers['Authorization'] = `Bearer ${cfg.apiKey}`;
|
|
371
|
+
break;
|
|
372
|
+
case 'header':
|
|
373
|
+
headers[def.authKey] = cfg.apiKey;
|
|
374
|
+
break;
|
|
375
|
+
case 'query':
|
|
376
|
+
url.searchParams.set(def.authKey, cfg.apiKey);
|
|
377
|
+
break;
|
|
378
|
+
case 'oauth2': {
|
|
379
|
+
const oauthCfg = cfg as ConnectorOAuthConfig;
|
|
380
|
+
const token = oauthCfg.accessToken ?? oauthCfg.apiKey;
|
|
381
|
+
// Use Bearer header — works for all methods and content types
|
|
382
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Forward request
|
|
388
|
+
const fetchOpts: RequestInit = {
|
|
389
|
+
method: request.method.toUpperCase(),
|
|
390
|
+
headers
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (request.body && !['GET', 'HEAD'].includes(fetchOpts.method!)) {
|
|
394
|
+
fetchOpts.body = JSON.stringify(request.body);
|
|
395
|
+
if (!headers['Content-Type']) {
|
|
396
|
+
headers['Content-Type'] = 'application/json';
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const upstream = await fetch(url.toString(), fetchOpts);
|
|
401
|
+
const ct = upstream.headers.get('content-type') || '';
|
|
402
|
+
const isJson = ct.includes('application/json');
|
|
403
|
+
|
|
404
|
+
if (!upstream.ok) {
|
|
405
|
+
const text = await upstream.text();
|
|
406
|
+
res.status(upstream.status).json({ error: text });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (isJson) {
|
|
411
|
+
const data = await upstream.json();
|
|
412
|
+
res.json(data);
|
|
413
|
+
} else {
|
|
414
|
+
// Stream binary-safe: use arrayBuffer to avoid text encoding corruption
|
|
415
|
+
const buffer = Buffer.from(await upstream.arrayBuffer());
|
|
416
|
+
res.set('Content-Type', ct || 'application/octet-stream');
|
|
417
|
+
// Forward content-disposition if present (e.g. file downloads)
|
|
418
|
+
const cd = upstream.headers.get('content-disposition');
|
|
419
|
+
if (cd) res.set('Content-Disposition', cd);
|
|
420
|
+
res.send(buffer);
|
|
421
|
+
}
|
|
422
|
+
} catch (err: unknown) {
|
|
423
|
+
console.error(err);
|
|
424
|
+
res.status(500).json({ error: (err as Error).message });
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Application } from 'express';
|
|
1
|
+
import { Application, Response } from 'express';
|
|
2
2
|
import { SynthOSConfig } from "../init";
|
|
3
3
|
import { checkIfExists, deleteFile, ensureFolderExists, listFiles, loadFile, saveFile } from "../files";
|
|
4
4
|
import path from "path";
|
|
@@ -6,90 +6,106 @@ import { v4 } from "uuid";
|
|
|
6
6
|
import { clearCachedScripts } from '../scripts';
|
|
7
7
|
|
|
8
8
|
export function useDataRoutes(config: SynthOSConfig, app: Application): void {
|
|
9
|
-
|
|
10
|
-
app.get('/api/data/:table',
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const ids = (await listFiles(folder)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
|
|
15
|
-
|
|
16
|
-
// Enumerate all rows
|
|
17
|
-
const rows: Record<string, any>[] = [];
|
|
18
|
-
for (const id of ids) {
|
|
19
|
-
const file = recordFile(folder, id);
|
|
20
|
-
try {
|
|
21
|
-
const row = JSON.parse(await loadFile(file));
|
|
22
|
-
row.id = id;
|
|
23
|
-
rows.push(row);
|
|
24
|
-
} catch (err: unknown) {
|
|
25
|
-
console.error(err);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Return rows
|
|
30
|
-
res.json(rows);
|
|
31
|
-
});
|
|
9
|
+
app.get('/api/data/:page/:table', (req, res) => handleList(config, req.params.page, req.params.table, req.query, res));
|
|
10
|
+
app.get('/api/data/:page/:table/:id', (req, res) => handleGet(config, req.params.page, req.params.table, req.params.id, res));
|
|
11
|
+
app.post('/api/data/:page/:table', (req, res) => handleUpsert(config, req.params.page, req.params.table, req.body, res));
|
|
12
|
+
app.delete('/api/data/:page/:table/:id', (req, res) => handleDelete(config, req.params.page, req.params.table, req.params.id, res));
|
|
13
|
+
}
|
|
32
14
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Route handlers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
async function handleList(config: SynthOSConfig, page: string, table: string, query: Record<string, any>, res: Response): Promise<void> {
|
|
20
|
+
const folder = tableFolder(config, page, table);
|
|
21
|
+
if (!(await checkIfExists(folder))) {
|
|
22
|
+
res.status(404).json({ error: 'table_not_found', page, table });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ids = (await listFiles(folder)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
|
|
27
|
+
|
|
28
|
+
const rows: Record<string, any>[] = [];
|
|
29
|
+
for (const id of ids) {
|
|
37
30
|
const file = recordFile(folder, id);
|
|
38
31
|
try {
|
|
39
32
|
const row = JSON.parse(await loadFile(file));
|
|
40
33
|
row.id = id;
|
|
41
|
-
|
|
42
|
-
} catch (err: unknown) {
|
|
43
|
-
res.json({});
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Upsert a single row into a table
|
|
48
|
-
app.post('/api/data/:table', async (req, res) => {
|
|
49
|
-
const { table } = req.params;
|
|
50
|
-
const id = req.body.id ?? v4();
|
|
51
|
-
const folder = await tableFolder(config, table);
|
|
52
|
-
const file = recordFile(folder, id);
|
|
53
|
-
try {
|
|
54
|
-
const row = { ...req.body, id };
|
|
55
|
-
await ensureFolderExists(folder);
|
|
56
|
-
await saveFile(file, JSON.stringify(row, null, 4));
|
|
57
|
-
if (table === 'scripts') {
|
|
58
|
-
clearCachedScripts();
|
|
59
|
-
}
|
|
60
|
-
res.json(row);
|
|
34
|
+
rows.push(row);
|
|
61
35
|
} catch (err: unknown) {
|
|
62
36
|
console.error(err);
|
|
63
|
-
res.status(500).send((err as Error).message);
|
|
64
37
|
}
|
|
65
|
-
}
|
|
38
|
+
}
|
|
66
39
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
40
|
+
// Paginate when limit is provided
|
|
41
|
+
const limitParam = typeof query.limit === 'string' ? parseInt(query.limit, 10) : NaN;
|
|
42
|
+
if (!isNaN(limitParam) && limitParam > 0) {
|
|
43
|
+
const offset = Math.max(0, typeof query.offset === 'string' ? parseInt(query.offset, 10) || 0 : 0);
|
|
44
|
+
const items = rows.slice(offset, offset + limitParam);
|
|
45
|
+
res.json({ items, total: rows.length, offset, limit: limitParam, hasMore: offset + limitParam < rows.length });
|
|
46
|
+
} else {
|
|
47
|
+
res.json(rows);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
75
50
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
51
|
+
async function handleGet(config: SynthOSConfig, page: string, table: string, id: string, res: Response): Promise<void> {
|
|
52
|
+
const folder = tableFolder(config, page, table);
|
|
53
|
+
if (!(await checkIfExists(folder))) {
|
|
54
|
+
res.status(404).json({ error: 'table_not_found', page, table });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const file = recordFile(folder, id);
|
|
59
|
+
try {
|
|
60
|
+
const row = JSON.parse(await loadFile(file));
|
|
61
|
+
row.id = id;
|
|
62
|
+
res.json(row);
|
|
63
|
+
} catch (err: unknown) {
|
|
64
|
+
res.json({});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function handleUpsert(config: SynthOSConfig, page: string, table: string, body: any, res: Response): Promise<void> {
|
|
69
|
+
const id = body.id ?? v4();
|
|
70
|
+
const folder = tableFolder(config, page, table);
|
|
71
|
+
const file = recordFile(folder, id);
|
|
72
|
+
try {
|
|
73
|
+
const row = { ...body, id };
|
|
74
|
+
await ensureFolderExists(folder);
|
|
75
|
+
await saveFile(file, JSON.stringify(row, null, 4));
|
|
76
|
+
if (table === 'scripts') {
|
|
77
|
+
clearCachedScripts();
|
|
78
|
+
}
|
|
79
|
+
res.json(row);
|
|
80
|
+
} catch (err: unknown) {
|
|
81
|
+
console.error(err);
|
|
82
|
+
res.status(500).send((err as Error).message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function handleDelete(config: SynthOSConfig, page: string, table: string, id: string, res: Response): Promise<void> {
|
|
87
|
+
const folder = tableFolder(config, page, table);
|
|
88
|
+
const file = recordFile(folder, id);
|
|
89
|
+
try {
|
|
90
|
+
if (await checkIfExists(file)) {
|
|
91
|
+
await deleteFile(file);
|
|
92
|
+
if (table === 'scripts') {
|
|
93
|
+
clearCachedScripts();
|
|
80
94
|
}
|
|
81
|
-
res.json({ success: true });
|
|
82
|
-
} catch (err: unknown) {
|
|
83
|
-
console.error(err);
|
|
84
|
-
res.status(500).send((err as Error).message);
|
|
85
95
|
}
|
|
86
|
-
|
|
96
|
+
res.json({ success: true });
|
|
97
|
+
} catch (err: unknown) {
|
|
98
|
+
console.error(err);
|
|
99
|
+
res.status(500).send((err as Error).message);
|
|
100
|
+
}
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Helpers
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
function tableFolder(config: SynthOSConfig, page: string, table: string): string {
|
|
108
|
+
return path.join(config.pagesFolder, 'pages', page, table);
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
function recordFile(folder: string, id: string): string {
|