synthos 0.10.0 → 0.11.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 +5 -5
- package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
- package/default-pages/elevenlabs_effects_studio/page.json +13 -11
- package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_voice_studio/page.html +782 -801
- package/default-pages/elevenlabs_voice_studio/page.json +13 -11
- package/default-pages/json_tools/chat-history.json +1 -0
- package/default-pages/json_tools/page.html +70 -90
- package/default-pages/json_tools/page.json +12 -10
- package/default-pages/my_notes/chat-history.json +1 -0
- package/default-pages/my_notes/page.html +115 -131
- package/default-pages/my_notes/page.json +14 -12
- package/default-pages/neon_asteroids/chat-history.json +1 -0
- package/default-pages/neon_asteroids/page.html +1777 -1803
- package/default-pages/neon_asteroids/page.json +14 -12
- package/default-pages/oregon_trail/chat-history.json +1 -0
- package/default-pages/oregon_trail/page.html +290 -307
- package/default-pages/oregon_trail/page.json +14 -12
- package/default-pages/solar_explorer/chat-history.json +1 -0
- package/default-pages/solar_explorer/page.html +1929 -1951
- package/default-pages/solar_explorer/page.json +14 -12
- package/default-pages/solar_tutorial/chat-history.json +1 -0
- package/default-pages/solar_tutorial/page.html +464 -478
- package/default-pages/solar_tutorial/page.json +12 -10
- package/default-pages/us_map/chat-history.json +1 -0
- package/default-pages/us_map/page.html +170 -193
- package/default-pages/us_map/page.json +14 -12
- package/default-pages/us_map/page.light.png +0 -0
- package/default-pages/us_map_1850/chat-history.json +1 -0
- package/default-pages/us_map_1850/page.html +302 -326
- package/default-pages/us_map_1850/page.json +14 -12
- package/default-pages/western_cities_1850/chat-history.json +1 -0
- package/default-pages/western_cities_1850/page.html +503 -527
- package/default-pages/western_cities_1850/page.json +14 -12
- package/default-themes/aurora-dawn.v3.css +15 -14
- package/default-themes/aurora-dusk.v3.css +26 -26
- package/default-themes/cosmos-dawn.v3.css +15 -14
- package/default-themes/cosmos-dusk.v3.css +26 -26
- package/default-themes/elemental-dawn.v3.css +200 -0
- package/default-themes/nebula-dawn.v3.css +15 -14
- package/default-themes/nebula-dusk.v3.css +24 -24
- package/default-themes/solar-flare-dawn.v3.css +15 -14
- package/default-themes/solar-flare-dusk.v3.css +26 -26
- package/dist/builders/anthropic.d.ts +26 -2
- package/dist/builders/anthropic.d.ts.map +1 -1
- package/dist/builders/anthropic.js +132 -31
- package/dist/builders/anthropic.js.map +1 -1
- package/dist/builders/claudecode.d.ts +13 -0
- package/dist/builders/claudecode.d.ts.map +1 -0
- package/dist/builders/claudecode.js +253 -0
- package/dist/builders/claudecode.js.map +1 -0
- package/dist/builders/index.d.ts +2 -1
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +8 -1
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/openai.js +2 -1
- package/dist/builders/openai.js.map +1 -1
- package/dist/builders/types.d.ts +31 -7
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +60 -28
- package/dist/builders/types.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +13 -6
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +161 -14
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +1 -0
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +129 -29
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/chainOfThought.d.ts.map +1 -1
- package/dist/models/chainOfThought.js +32 -19
- package/dist/models/chainOfThought.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +15 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +57 -8
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +258 -45
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +5 -0
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/mediaCache.d.ts +36 -0
- package/dist/service/mediaCache.d.ts.map +1 -0
- package/dist/service/mediaCache.js +182 -0
- package/dist/service/mediaCache.js.map +1 -0
- package/dist/service/pageValidator.d.ts +25 -0
- package/dist/service/pageValidator.d.ts.map +1 -0
- package/dist/service/pageValidator.js +315 -0
- package/dist/service/pageValidator.js.map +1 -0
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +4 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/sharedTableSchema.d.ts +73 -0
- package/dist/service/sharedTableSchema.d.ts.map +1 -0
- package/dist/service/sharedTableSchema.js +206 -0
- package/dist/service/sharedTableSchema.js.map +1 -0
- package/dist/service/transformPage.d.ts +49 -11
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +354 -241
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +288 -34
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +170 -32
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +59 -2
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useExtractRoutes.d.ts +4 -0
- package/dist/service/useExtractRoutes.d.ts.map +1 -0
- package/dist/service/useExtractRoutes.js +304 -0
- package/dist/service/useExtractRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +17 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +1385 -483
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +54 -2
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/settings.d.ts +27 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +40 -1
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +0 -5
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +3 -95
- package/dist/themes.js.map +1 -1
- package/migration-rules/v2-to-v3.md +277 -119
- package/package.json +5 -1
- package/{default-pages/application → required-pages/_shell}/page.html +56 -42
- package/required-pages/_shell/page.json +14 -0
- package/required-pages/_starters/page.html +534 -0
- package/required-pages/_starters/page.json +12 -0
- package/required-pages/builder/page.html +353 -43
- package/required-pages/builder/page.json +12 -10
- package/required-pages/pages/page.html +697 -924
- package/required-pages/pages/page.json +12 -10
- package/required-pages/settings/page.html +1879 -1753
- package/required-pages/settings/page.json +12 -10
- package/required-pages/synthos_apis/page.html +834 -845
- package/required-pages/synthos_apis/page.json +12 -10
- package/required-pages/synthos_scripts/page.html +74 -88
- package/required-pages/synthos_scripts/page.json +12 -10
- package/scripts/append-instructions.py +90 -0
- package/scripts/audit-instructions.py +76 -0
- package/scripts/cleanup-shell-markup.mjs +112 -0
- package/service-connectors/buffer/connector.json +46 -0
- package/service-connectors/canva/connector.json +67 -0
- package/service-connectors/elevenlabs/connector.json +1 -1
- package/src/builders/anthropic.ts +155 -30
- package/src/builders/claudecode.ts +310 -0
- package/src/builders/index.ts +7 -1
- package/src/builders/openai.ts +2 -1
- package/src/builders/types.ts +93 -32
- package/src/connectors/types.ts +8 -0
- package/src/init.ts +13 -7
- package/src/migrations.ts +187 -16
- package/src/models/anthropic.ts +140 -30
- package/src/models/chainOfThought.ts +33 -18
- package/src/models/index.ts +2 -2
- package/src/models/providers.ts +12 -3
- package/src/models/types.ts +21 -1
- package/src/pages.ts +271 -35
- package/src/service/createCompletePrompt.ts +6 -0
- package/src/service/mediaCache.ts +206 -0
- package/src/service/pageValidator.ts +337 -0
- package/src/service/server.ts +4 -0
- package/src/service/sharedTableSchema.ts +236 -0
- package/src/service/transformPage.ts +370 -260
- package/src/service/useApiRoutes.ts +282 -32
- package/src/service/useConnectorRoutes.ts +189 -34
- package/src/service/useDataRoutes.ts +198 -116
- package/src/service/useExtractRoutes.ts +331 -0
- package/src/service/usePageRoutes.ts +1411 -394
- package/src/service/useSharedDataRoutes.ts +184 -109
- package/src/settings.ts +65 -0
- package/src/themes.ts +78 -180
- package/starters/blank_starter/chat-history.json +1 -0
- package/starters/blank_starter/page.dark.png +0 -0
- package/starters/blank_starter/page.html +47 -0
- package/starters/blank_starter/page.json +13 -0
- package/starters/blank_starter/page.light.png +0 -0
- package/starters/calculator_starter/chat-history.json +1 -0
- package/starters/calculator_starter/page.dark.png +0 -0
- package/starters/calculator_starter/page.html +232 -0
- package/starters/calculator_starter/page.json +13 -0
- package/starters/calculator_starter/page.light.png +0 -0
- package/starters/calendar_starter/chat-history.json +1 -0
- package/starters/calendar_starter/page.dark.png +0 -0
- package/starters/calendar_starter/page.html +495 -0
- package/starters/calendar_starter/page.json +13 -0
- package/starters/calendar_starter/page.light.png +0 -0
- package/starters/chat_starter/chat-history.json +1 -0
- package/starters/chat_starter/page.dark.png +0 -0
- package/starters/chat_starter/page.html +351 -0
- package/starters/chat_starter/page.json +13 -0
- package/starters/chat_starter/page.light.png +0 -0
- package/starters/checklist_starter/chat-history.json +1 -0
- package/starters/checklist_starter/page.dark.png +0 -0
- package/starters/checklist_starter/page.html +437 -0
- package/starters/checklist_starter/page.json +13 -0
- package/starters/checklist_starter/page.light.png +0 -0
- package/starters/dashboard_starter/chat-history.json +1 -0
- package/starters/dashboard_starter/page.dark.png +0 -0
- package/starters/dashboard_starter/page.html +195 -0
- package/starters/dashboard_starter/page.json +13 -0
- package/starters/dashboard_starter/page.light.png +0 -0
- package/starters/form_starter/chat-history.json +1 -0
- package/starters/form_starter/page.dark.png +0 -0
- package/starters/form_starter/page.html +313 -0
- package/starters/form_starter/page.json +13 -0
- package/starters/form_starter/page.light.png +0 -0
- package/starters/gallery_starter/chat-history.json +1 -0
- package/starters/gallery_starter/page.dark.png +0 -0
- package/starters/gallery_starter/page.html +418 -0
- package/starters/gallery_starter/page.json +13 -0
- package/starters/gallery_starter/page.light.png +0 -0
- package/starters/generator_starter/chat-history.json +1 -0
- package/starters/generator_starter/page.dark.png +0 -0
- package/starters/generator_starter/page.html +261 -0
- package/starters/generator_starter/page.json +13 -0
- package/starters/generator_starter/page.light.png +0 -0
- package/starters/index.html +538 -0
- package/starters/kanban_starter/chat-history.json +1 -0
- package/starters/kanban_starter/page.dark.png +0 -0
- package/starters/kanban_starter/page.html +432 -0
- package/starters/kanban_starter/page.json +13 -0
- package/starters/kanban_starter/page.light.png +0 -0
- package/starters/presentation_builder/chat-history.json +1 -0
- package/starters/presentation_builder/page.dark.png +0 -0
- package/starters/presentation_builder/page.html +970 -0
- package/starters/presentation_builder/page.json +15 -0
- package/starters/presentation_builder/page.light.png +0 -0
- package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
- package/starters/pulse_starter/chat-history.json +1 -0
- package/starters/pulse_starter/page.dark.png +0 -0
- package/starters/pulse_starter/page.html +698 -0
- package/starters/pulse_starter/page.json +13 -0
- package/starters/pulse_starter/page.light.png +0 -0
- package/starters/quiz_starter/chat-history.json +1 -0
- package/starters/quiz_starter/page.dark.png +0 -0
- package/starters/quiz_starter/page.html +292 -0
- package/starters/quiz_starter/page.json +13 -0
- package/starters/quiz_starter/page.light.png +0 -0
- package/starters/reference_starter/chat-history.json +1 -0
- package/starters/reference_starter/page.dark.png +0 -0
- package/starters/reference_starter/page.html +250 -0
- package/starters/reference_starter/page.json +13 -0
- package/starters/reference_starter/page.light.png +0 -0
- package/starters/retro_game_starter/chat-history.json +1 -0
- package/starters/retro_game_starter/page.dark.png +0 -0
- package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
- package/starters/retro_game_starter/page.json +15 -0
- package/starters/retro_game_starter/page.light.png +0 -0
- package/starters/roster_starter/chat-history.json +1 -0
- package/starters/roster_starter/page.dark.png +0 -0
- package/starters/roster_starter/page.html +600 -0
- package/starters/roster_starter/page.json +13 -0
- package/starters/roster_starter/page.light.png +0 -0
- package/starters/server.js +182 -0
- package/starters/start.cmd +1 -0
- package/starters/timeline_starter/chat-history.json +1 -0
- package/starters/timeline_starter/page.dark.png +0 -0
- package/starters/timeline_starter/page.html +446 -0
- package/starters/timeline_starter/page.json +13 -0
- package/starters/timeline_starter/page.light.png +0 -0
- package/starters/tutorial_starter/chat-history.json +1 -0
- package/starters/tutorial_starter/page.dark.png +0 -0
- package/starters/tutorial_starter/page.html +283 -0
- package/starters/tutorial_starter/page.json +13 -0
- package/starters/tutorial_starter/page.light.png +0 -0
- package/static-files/agent.v3.js +122 -0
- package/static-files/connector.v3.js +48 -0
- package/static-files/extract.v3.js +188 -0
- package/static-files/helpers.v3.js +50 -6
- package/static-files/page-bridge.js +114 -0
- package/static-files/page.v3.js +1292 -1290
- package/static-files/script.v3.js +32 -0
- package/static-files/server.v3.js +89 -0
- package/static-files/shell-bridge.v3.js +174 -0
- package/static-files/shell-modals.v3.js +521 -0
- package/static-files/{shell.css → shell.v3.css} +271 -22
- package/static-files/shell.v3.js +1865 -0
- package/static-files/storage.v3.js +176 -0
- package/tests/anthropic.spec.ts +42 -7
- package/tests/builders.spec.ts +72 -4
- package/tests/pageValidator.spec.ts +548 -0
- package/tests/profiles.spec.ts +122 -0
- package/tests/providers.spec.ts +1 -1
- package/tests/sharedTableSchema.spec.ts +242 -0
- package/tests/transformPage.spec.ts +62 -81
- package/default-pages/application/page.json +0 -10
- package/default-pages/retro_game_starter/page.json +0 -12
- package/default-pages/sidebar_page/page.html +0 -51
- package/default-pages/sidebar_page/page.json +0 -10
- package/default-pages/two-panel_page/page.html +0 -68
- package/default-pages/two-panel_page/page.json +0 -10
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import crypto from 'crypto';
|
|
1
3
|
import { Application } from 'express';
|
|
2
4
|
import { SynthOSConfig } from '../init';
|
|
3
5
|
import { loadSettings, saveSettings } from '../settings';
|
|
@@ -6,10 +8,87 @@ import {
|
|
|
6
8
|
ConnectorSummary,
|
|
7
9
|
ConnectorDetail,
|
|
8
10
|
ConnectorCallRequest,
|
|
11
|
+
ConnectorDefinition,
|
|
9
12
|
ConnectorOAuthConfig
|
|
10
13
|
} from '../connectors';
|
|
14
|
+
import { createMediaCache } from './mediaCache';
|
|
15
|
+
|
|
16
|
+
function base64url(buf: Buffer): string {
|
|
17
|
+
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function generateCodeVerifier(): string {
|
|
21
|
+
return base64url(crypto.randomBytes(32));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function generateCodeChallenge(verifier: string): string {
|
|
25
|
+
return base64url(crypto.createHash('sha256').update(verifier).digest());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface TokenResponse {
|
|
29
|
+
access_token: string;
|
|
30
|
+
refresh_token?: string;
|
|
31
|
+
token_type?: string;
|
|
32
|
+
expires_in?: number;
|
|
33
|
+
scope?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* RFC 6749 §4.1.3 / §6: token endpoint requires application/x-www-form-urlencoded POST.
|
|
38
|
+
* Public clients (no client_secret) authenticate via PKCE alone; confidential clients send both.
|
|
39
|
+
*/
|
|
40
|
+
async function exchangeOAuthToken(
|
|
41
|
+
tokenUrl: string,
|
|
42
|
+
params: Record<string, string | undefined>
|
|
43
|
+
): Promise<{ ok: boolean; data?: TokenResponse; errorText?: string }> {
|
|
44
|
+
const body = new URLSearchParams();
|
|
45
|
+
for (const [k, v] of Object.entries(params)) {
|
|
46
|
+
if (typeof v === 'string' && v.length > 0) body.set(k, v);
|
|
47
|
+
}
|
|
48
|
+
const res = await fetch(tokenUrl, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },
|
|
51
|
+
body: body.toString()
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
return { ok: false, errorText: await res.text() };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, data: await res.json() as TokenResponse };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Refresh an expired access token. Buffer rotates refresh tokens — always save the latest.
|
|
61
|
+
* Returns the updated config slice (caller persists). Returns null on failure.
|
|
62
|
+
*/
|
|
63
|
+
async function refreshAccessToken(
|
|
64
|
+
def: ConnectorDefinition,
|
|
65
|
+
cfg: ConnectorOAuthConfig
|
|
66
|
+
): Promise<Partial<ConnectorOAuthConfig> | null> {
|
|
67
|
+
if (!def.tokenUrl || !cfg.refreshToken || !cfg.clientId) return null;
|
|
68
|
+
const result = await exchangeOAuthToken(def.tokenUrl, {
|
|
69
|
+
grant_type: 'refresh_token',
|
|
70
|
+
refresh_token: cfg.refreshToken,
|
|
71
|
+
client_id: cfg.clientId,
|
|
72
|
+
client_secret: cfg.clientSecret
|
|
73
|
+
});
|
|
74
|
+
if (!result.ok || !result.data) {
|
|
75
|
+
console.error(`Token refresh failed for "${def.id}":`, result.errorText);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const data = result.data;
|
|
79
|
+
return {
|
|
80
|
+
accessToken: data.access_token,
|
|
81
|
+
apiKey: data.access_token,
|
|
82
|
+
refreshToken: data.refresh_token ?? cfg.refreshToken,
|
|
83
|
+
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : 0
|
|
84
|
+
};
|
|
85
|
+
}
|
|
11
86
|
|
|
12
87
|
export function useConnectorRoutes(config: SynthOSConfig, app: Application): void {
|
|
88
|
+
const mediaCache = createMediaCache({
|
|
89
|
+
storage: config.storageProvider,
|
|
90
|
+
cacheRoot: path.join(config.pagesFolder, 'cache'),
|
|
91
|
+
});
|
|
13
92
|
|
|
14
93
|
// GET /api/connectors — List connectors (minimal summaries)
|
|
15
94
|
// Also handles POST /api/connectors — Proxy call (see below)
|
|
@@ -172,8 +251,14 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
172
251
|
|
|
173
252
|
const settings = await loadSettings(config);
|
|
174
253
|
const cfg = (settings.connectors ?? {})[id] as ConnectorOAuthConfig | undefined;
|
|
175
|
-
|
|
176
|
-
|
|
254
|
+
// PKCE-only public clients may register without a client_secret. Require clientId always;
|
|
255
|
+
// require clientSecret only for non-PKCE flows.
|
|
256
|
+
if (!cfg?.clientId) {
|
|
257
|
+
res.status(400).json({ error: 'Client ID must be saved before authorizing' });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!def.usePkce && !cfg.clientSecret) {
|
|
261
|
+
res.status(400).json({ error: 'Client Secret must be saved before authorizing' });
|
|
177
262
|
return;
|
|
178
263
|
}
|
|
179
264
|
|
|
@@ -183,10 +268,22 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
183
268
|
const authUrl = new URL(def.authorizationUrl!);
|
|
184
269
|
authUrl.searchParams.set('client_id', cfg.clientId);
|
|
185
270
|
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
186
|
-
authUrl.searchParams.set('scope', (def.scopes ?? []).join(','));
|
|
271
|
+
authUrl.searchParams.set('scope', (def.scopes ?? []).join(def.scopeSeparator ?? ','));
|
|
187
272
|
authUrl.searchParams.set('state', state);
|
|
188
273
|
authUrl.searchParams.set('response_type', 'code');
|
|
189
274
|
|
|
275
|
+
// PKCE: generate verifier+challenge, persist verifier for the /callback step
|
|
276
|
+
if (def.usePkce) {
|
|
277
|
+
const codeVerifier = generateCodeVerifier();
|
|
278
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
279
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
280
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
281
|
+
|
|
282
|
+
const existing = settings.connectors ?? {};
|
|
283
|
+
const updated = { ...existing, [id]: { ...cfg, codeVerifier } };
|
|
284
|
+
await saveSettings(config, { connectors: updated as typeof existing });
|
|
285
|
+
}
|
|
286
|
+
|
|
190
287
|
res.redirect(authUrl.toString());
|
|
191
288
|
} catch (err: unknown) {
|
|
192
289
|
console.error(err);
|
|
@@ -222,38 +319,45 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
222
319
|
|
|
223
320
|
const settings = await loadSettings(config);
|
|
224
321
|
const cfg = (settings.connectors ?? {})[connectorId] as ConnectorOAuthConfig | undefined;
|
|
225
|
-
if (!cfg?.clientId
|
|
322
|
+
if (!cfg?.clientId) {
|
|
323
|
+
res.status(400).json({ error: 'Client credentials not found' });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!def.usePkce && !cfg.clientSecret) {
|
|
226
327
|
res.status(400).json({ error: 'Client credentials not found' });
|
|
227
328
|
return;
|
|
228
329
|
}
|
|
229
330
|
|
|
230
331
|
const redirectUri = `${req.protocol}://${req.get('host')}/api/connectors/callback`;
|
|
231
332
|
|
|
232
|
-
// Step 1: Exchange code for short-lived token
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
333
|
+
// Step 1: Exchange code for short-lived token (RFC 6749 §4.1.3 — POST + form body)
|
|
334
|
+
const tokenResult = await exchangeOAuthToken(def.tokenUrl!, {
|
|
335
|
+
grant_type: 'authorization_code',
|
|
336
|
+
code,
|
|
337
|
+
redirect_uri: redirectUri,
|
|
338
|
+
client_id: cfg.clientId,
|
|
339
|
+
// Public PKCE clients omit client_secret; confidential clients send both.
|
|
340
|
+
client_secret: cfg.clientSecret,
|
|
341
|
+
// PKCE: include verifier when one was stored at /authorize
|
|
342
|
+
code_verifier: def.usePkce ? cfg.codeVerifier : undefined
|
|
343
|
+
});
|
|
344
|
+
if (!tokenResult.ok || !tokenResult.data) {
|
|
345
|
+
console.error('Token exchange failed:', tokenResult.errorText);
|
|
244
346
|
res.redirect(`/settings?tab=connectors&error=${encodeURIComponent('Token exchange failed')}`);
|
|
245
347
|
return;
|
|
246
348
|
}
|
|
247
|
-
const tokenData =
|
|
349
|
+
const tokenData = tokenResult.data;
|
|
248
350
|
let accessToken = tokenData.access_token;
|
|
351
|
+
let refreshToken = tokenData.refresh_token;
|
|
249
352
|
let expiresAt = tokenData.expires_in ? Date.now() + tokenData.expires_in * 1000 : 0;
|
|
250
353
|
|
|
251
|
-
// Step 2: For Instagram — exchange for long-lived token
|
|
354
|
+
// Step 2: For Instagram — exchange for long-lived token (Facebook is non-PKCE,
|
|
355
|
+
// so clientSecret is guaranteed by the earlier non-PKCE branch validation)
|
|
252
356
|
if (connectorId === 'instagram') {
|
|
253
357
|
const llUrl = new URL('https://graph.facebook.com/v21.0/oauth/access_token');
|
|
254
358
|
llUrl.searchParams.set('grant_type', 'fb_exchange_token');
|
|
255
359
|
llUrl.searchParams.set('client_id', cfg.clientId);
|
|
256
|
-
llUrl.searchParams.set('client_secret', cfg.clientSecret);
|
|
360
|
+
llUrl.searchParams.set('client_secret', cfg.clientSecret!);
|
|
257
361
|
llUrl.searchParams.set('fb_exchange_token', accessToken);
|
|
258
362
|
|
|
259
363
|
const llRes = await fetch(llUrl.toString());
|
|
@@ -292,22 +396,22 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
292
396
|
}
|
|
293
397
|
}
|
|
294
398
|
|
|
295
|
-
// Step 4: Save tokens to settings
|
|
399
|
+
// Step 4: Save tokens to settings. Drop codeVerifier — it's single-use per RFC 7636.
|
|
296
400
|
const existing = settings.connectors ?? {};
|
|
297
401
|
const prev = (existing[connectorId] as ConnectorOAuthConfig) ?? { apiKey: '', enabled: false };
|
|
298
|
-
const
|
|
299
|
-
...
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
accountName,
|
|
307
|
-
enabled: true
|
|
308
|
-
}
|
|
402
|
+
const next: ConnectorOAuthConfig = {
|
|
403
|
+
...prev,
|
|
404
|
+
apiKey: prev.apiKey || accessToken,
|
|
405
|
+
accessToken,
|
|
406
|
+
expiresAt,
|
|
407
|
+
userId,
|
|
408
|
+
accountName,
|
|
409
|
+
enabled: true
|
|
309
410
|
};
|
|
310
|
-
|
|
411
|
+
if (refreshToken) next.refreshToken = refreshToken;
|
|
412
|
+
delete next.codeVerifier;
|
|
413
|
+
const updatedConnectors = { ...existing, [connectorId]: next };
|
|
414
|
+
await saveSettings(config, { connectors: updatedConnectors as typeof existing });
|
|
311
415
|
|
|
312
416
|
res.redirect(`/settings?tab=connectors&connected=${encodeURIComponent(connectorId)}`);
|
|
313
417
|
} catch (err: unknown) {
|
|
@@ -341,8 +445,22 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
341
445
|
res.status(400).json({ error: `Connector "${request.connector}" is not configured or not enabled` });
|
|
342
446
|
return;
|
|
343
447
|
}
|
|
344
|
-
//
|
|
345
|
-
|
|
448
|
+
// Auto-refresh if expired (or expiring within 60s) and a refresh_token is available.
|
|
449
|
+
// Skip for connectors without a refresh_token (e.g. Instagram long-lived) — fall through to the 401.
|
|
450
|
+
const expiringSoon = oauthCfg.expiresAt && oauthCfg.expiresAt < Date.now() + 60_000;
|
|
451
|
+
if (expiringSoon && oauthCfg.refreshToken) {
|
|
452
|
+
const refreshed = await refreshAccessToken(def, oauthCfg);
|
|
453
|
+
if (refreshed) {
|
|
454
|
+
const existing = settings.connectors ?? {};
|
|
455
|
+
const merged = { ...oauthCfg, ...refreshed };
|
|
456
|
+
const updated = { ...existing, [request.connector]: merged };
|
|
457
|
+
await saveSettings(config, { connectors: updated as typeof existing });
|
|
458
|
+
Object.assign(oauthCfg, refreshed);
|
|
459
|
+
} else {
|
|
460
|
+
res.status(401).json({ error: `Access token for "${request.connector}" expired and refresh failed. Please re-authorize in Settings > Connectors.` });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
} else if (expiringSoon) {
|
|
346
464
|
res.status(401).json({ error: `Access token for "${request.connector}" has expired. Please re-authorize in Settings > Connectors.` });
|
|
347
465
|
return;
|
|
348
466
|
}
|
|
@@ -353,6 +471,29 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
353
471
|
}
|
|
354
472
|
}
|
|
355
473
|
|
|
474
|
+
// Detect TTS calls eligible for audio caching
|
|
475
|
+
const isTts = request.connector === 'elevenlabs'
|
|
476
|
+
&& request.method.toUpperCase() === 'POST'
|
|
477
|
+
&& request.path.startsWith('/v1/text-to-speech/');
|
|
478
|
+
const cacheEnabled = settings.cache?.enabled !== false;
|
|
479
|
+
const noCache = req.headers['x-no-cache'] === 'true' || req.query.nocache === '1';
|
|
480
|
+
|
|
481
|
+
// Check audio cache for TTS requests
|
|
482
|
+
if (isTts && cacheEnabled && !noCache) {
|
|
483
|
+
const voiceId = request.path.split('/v1/text-to-speech/')[1]?.split('?')[0] ?? '';
|
|
484
|
+
const outputFormat = (request.query?.output_format) ?? 'mp3_44100_128';
|
|
485
|
+
const text = typeof request.body === 'object' && request.body !== null
|
|
486
|
+
? (request.body as Record<string, unknown>).text ?? ''
|
|
487
|
+
: '';
|
|
488
|
+
const cacheKey = `audio:v1:${request.connector}:${voiceId}:${outputFormat}:${text}`;
|
|
489
|
+
const cached = await mediaCache.get('audio', cacheKey);
|
|
490
|
+
if (cached.hit) {
|
|
491
|
+
res.set('Content-Type', cached.contentType);
|
|
492
|
+
res.send(cached.buffer);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
356
497
|
// Build URL — join baseUrl path with request path to avoid
|
|
357
498
|
// absolute paths (e.g. "/me/accounts") replacing the base path.
|
|
358
499
|
// Split path from inline query string first — assigning a '?' to
|
|
@@ -428,6 +569,20 @@ export function useConnectorRoutes(config: SynthOSConfig, app: Application): voi
|
|
|
428
569
|
// Forward content-disposition if present (e.g. file downloads)
|
|
429
570
|
const cd = upstream.headers.get('content-disposition');
|
|
430
571
|
if (cd) res.set('Content-Disposition', cd);
|
|
572
|
+
|
|
573
|
+
// Cache TTS audio responses
|
|
574
|
+
if (isTts && cacheEnabled) {
|
|
575
|
+
const voiceId = request.path.split('/v1/text-to-speech/')[1]?.split('?')[0] ?? '';
|
|
576
|
+
const outputFormat = (request.query?.output_format) ?? 'mp3_44100_128';
|
|
577
|
+
const text = typeof request.body === 'object' && request.body !== null
|
|
578
|
+
? (request.body as Record<string, unknown>).text ?? ''
|
|
579
|
+
: '';
|
|
580
|
+
const cacheKey = `audio:v1:${request.connector}:${voiceId}:${outputFormat}:${text}`;
|
|
581
|
+
await mediaCache.put('audio', cacheKey, buffer, ct || 'audio/mpeg', {
|
|
582
|
+
connector: request.connector, voiceId, outputFormat, text,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
431
586
|
res.send(buffer);
|
|
432
587
|
}
|
|
433
588
|
} catch (err: unknown) {
|
|
@@ -1,116 +1,198 @@
|
|
|
1
|
-
import { Application, Response } from 'express';
|
|
2
|
-
import { SynthOSConfig } from "../init";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { v4 } from "uuid";
|
|
5
|
-
import { clearCachedScripts } from '../scripts';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const folder = tableFolder(config, page, table);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
res.
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function
|
|
89
|
-
const sp = config.storageProvider;
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
1
|
+
import { Application, Response } from 'express';
|
|
2
|
+
import { SynthOSConfig } from "../init";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { v4 } from "uuid";
|
|
5
|
+
import { clearCachedScripts } from '../scripts';
|
|
6
|
+
import {
|
|
7
|
+
deleteSchema,
|
|
8
|
+
isValidSchemaPayload,
|
|
9
|
+
listTables,
|
|
10
|
+
loadSchema,
|
|
11
|
+
mergeSchema,
|
|
12
|
+
newSchemaWrapper,
|
|
13
|
+
saveSchema,
|
|
14
|
+
updateSchemaWrapper,
|
|
15
|
+
MergeMode,
|
|
16
|
+
} from "./sharedTableSchema";
|
|
17
|
+
|
|
18
|
+
export function useDataRoutes(config: SynthOSConfig, app: Application): void {
|
|
19
|
+
// Schema sidecar + table-list endpoints registered before the generic
|
|
20
|
+
// table/record routes so the literal `_schema` / `_tables` segments
|
|
21
|
+
// aren't captured by `:table` / `:table/:id`.
|
|
22
|
+
app.get('/api/data/:page/_tables', (req, res) => handleListTables(config, req.params.page, res));
|
|
23
|
+
app.get('/api/data/:page/:table/_schema', (req, res) => handleGetSchema(config, req.params.page, req.params.table, res));
|
|
24
|
+
app.put('/api/data/:page/:table/_schema', (req, res) => handlePutSchema(config, req.params.page, req.params.table, req.query, req.body, res));
|
|
25
|
+
app.delete('/api/data/:page/:table/_schema', (req, res) => handleDeleteSchema(config, req.params.page, req.params.table, res));
|
|
26
|
+
|
|
27
|
+
app.get('/api/data/:page/:table', (req, res) => handleList(config, req.params.page, req.params.table, req.query, res));
|
|
28
|
+
app.get('/api/data/:page/:table/:id', (req, res) => handleGet(config, req.params.page, req.params.table, req.params.id, res));
|
|
29
|
+
app.post('/api/data/:page/:table', (req, res) => handleUpsert(config, req.params.page, req.params.table, req.body, res));
|
|
30
|
+
app.delete('/api/data/:page/:table/:id', (req, res) => handleDelete(config, req.params.page, req.params.table, req.params.id, res));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Route handlers — records
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
async function handleList(config: SynthOSConfig, page: string, table: string, query: Record<string, any>, res: Response): Promise<void> {
|
|
38
|
+
const sp = config.storageProvider;
|
|
39
|
+
const folder = tableFolder(config, page, table);
|
|
40
|
+
if (!(await sp.checkIfExists(folder))) {
|
|
41
|
+
res.status(404).json({ error: 'table_not_found', page, table });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ids = (await sp.listFiles(folder)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
|
|
46
|
+
|
|
47
|
+
const rows: Record<string, any>[] = [];
|
|
48
|
+
for (const id of ids) {
|
|
49
|
+
const file = recordFile(folder, id);
|
|
50
|
+
try {
|
|
51
|
+
const row = JSON.parse(await sp.loadFile(file));
|
|
52
|
+
row.id = id;
|
|
53
|
+
rows.push(row);
|
|
54
|
+
} catch (err: unknown) {
|
|
55
|
+
console.error(err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Paginate when limit is provided
|
|
60
|
+
const limitParam = typeof query.limit === 'string' ? parseInt(query.limit, 10) : NaN;
|
|
61
|
+
if (!isNaN(limitParam) && limitParam > 0) {
|
|
62
|
+
const offset = Math.max(0, typeof query.offset === 'string' ? parseInt(query.offset, 10) || 0 : 0);
|
|
63
|
+
const items = rows.slice(offset, offset + limitParam);
|
|
64
|
+
res.json({ items, total: rows.length, offset, limit: limitParam, hasMore: offset + limitParam < rows.length });
|
|
65
|
+
} else {
|
|
66
|
+
res.json(rows);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function handleGet(config: SynthOSConfig, page: string, table: string, id: string, res: Response): Promise<void> {
|
|
71
|
+
const sp = config.storageProvider;
|
|
72
|
+
const folder = tableFolder(config, page, table);
|
|
73
|
+
if (!(await sp.checkIfExists(folder))) {
|
|
74
|
+
res.status(404).json({ error: 'table_not_found', page, table });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const file = recordFile(folder, id);
|
|
79
|
+
try {
|
|
80
|
+
const row = JSON.parse(await sp.loadFile(file));
|
|
81
|
+
row.id = id;
|
|
82
|
+
res.json(row);
|
|
83
|
+
} catch (err: unknown) {
|
|
84
|
+
res.json({});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function handleUpsert(config: SynthOSConfig, page: string, table: string, body: any, res: Response): Promise<void> {
|
|
89
|
+
const sp = config.storageProvider;
|
|
90
|
+
const id = body.id ?? v4();
|
|
91
|
+
const folder = tableFolder(config, page, table);
|
|
92
|
+
const file = recordFile(folder, id);
|
|
93
|
+
try {
|
|
94
|
+
const row = { ...body, id };
|
|
95
|
+
await sp.ensureFolderExists(folder);
|
|
96
|
+
await sp.saveFile(file, JSON.stringify(row, null, 4));
|
|
97
|
+
if (table === 'scripts') {
|
|
98
|
+
clearCachedScripts();
|
|
99
|
+
}
|
|
100
|
+
res.json(row);
|
|
101
|
+
} catch (err: unknown) {
|
|
102
|
+
console.error(err);
|
|
103
|
+
res.status(500).send((err as Error).message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function handleDelete(config: SynthOSConfig, page: string, table: string, id: string, res: Response): Promise<void> {
|
|
108
|
+
const sp = config.storageProvider;
|
|
109
|
+
const folder = tableFolder(config, page, table);
|
|
110
|
+
const file = recordFile(folder, id);
|
|
111
|
+
try {
|
|
112
|
+
if (await sp.checkIfExists(file)) {
|
|
113
|
+
await sp.deleteFile(file);
|
|
114
|
+
if (table === 'scripts') {
|
|
115
|
+
clearCachedScripts();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
res.json({ success: true });
|
|
119
|
+
} catch (err: unknown) {
|
|
120
|
+
console.error(err);
|
|
121
|
+
res.status(500).send((err as Error).message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Route handlers — schema sidecar
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
async function handleGetSchema(config: SynthOSConfig, page: string, table: string, res: Response): Promise<void> {
|
|
130
|
+
const wrapper = await loadSchema(config, pageNamespace(config, page), table);
|
|
131
|
+
if (!wrapper) {
|
|
132
|
+
res.status(404).json({ error: 'schema_not_found', page, table });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
res.json(wrapper);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function handlePutSchema(
|
|
139
|
+
config: SynthOSConfig,
|
|
140
|
+
page: string,
|
|
141
|
+
table: string,
|
|
142
|
+
query: Record<string, any>,
|
|
143
|
+
body: any,
|
|
144
|
+
res: Response,
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
const incoming = body && typeof body === 'object' && body.schema ? body.schema : body;
|
|
147
|
+
if (!isValidSchemaPayload(incoming)) {
|
|
148
|
+
res.status(400).json({ error: 'invalid_schema', message: 'Body must be a JSON Schema object (or { schema: ... } wrapper).' });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const merge: MergeMode = query.merge === 'replace' ? 'replace' : 'additive';
|
|
152
|
+
const definedBy = typeof body?.definedBy === 'string' ? body.definedBy : undefined;
|
|
153
|
+
const namespace = pageNamespace(config, page);
|
|
154
|
+
const existing = await loadSchema(config, namespace, table);
|
|
155
|
+
const { merged, conflicts } = mergeSchema(existing?.schema, incoming, merge);
|
|
156
|
+
if (conflicts.length > 0) {
|
|
157
|
+
res.status(409).json({ error: 'schema_conflict', conflicts });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const now = new Date().toISOString();
|
|
161
|
+
const wrapper = existing
|
|
162
|
+
? updateSchemaWrapper(existing, merged, now, definedBy)
|
|
163
|
+
: newSchemaWrapper(merged, now, definedBy);
|
|
164
|
+
await saveSchema(config, namespace, table, wrapper);
|
|
165
|
+
res.json(wrapper);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function handleDeleteSchema(config: SynthOSConfig, page: string, table: string, res: Response): Promise<void> {
|
|
169
|
+
await deleteSchema(config, pageNamespace(config, page), table);
|
|
170
|
+
res.status(204).end();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Subfolders under `<pagesFolder>/pages/<page>/` that are NOT data tables.
|
|
175
|
+
* `files` is the synthos.files namespace; it must not appear in the table list.
|
|
176
|
+
*/
|
|
177
|
+
const RESERVED_PAGE_SUBDIRS: ReadonlySet<string> = new Set(['files']);
|
|
178
|
+
|
|
179
|
+
async function handleListTables(config: SynthOSConfig, page: string, res: Response): Promise<void> {
|
|
180
|
+
const tables = await listTables(config, pageNamespace(config, page), RESERVED_PAGE_SUBDIRS);
|
|
181
|
+
res.json({ tables });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Helpers
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function pageNamespace(config: SynthOSConfig, page: string): string {
|
|
189
|
+
return path.join(config.pagesFolder, 'pages', page);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function tableFolder(config: SynthOSConfig, page: string, table: string): string {
|
|
193
|
+
return path.join(pageNamespace(config, page), table);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function recordFile(folder: string, id: string): string {
|
|
197
|
+
return path.join(folder, `${id}.json`);
|
|
198
|
+
}
|