@odvi/create-dtt-framework 0.1.12 → 0.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odvi/create-dtt-framework",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "CLI tool to scaffold new projects with DTT Framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -104,6 +104,19 @@ AWS_ACCESS_SECRET_KEY=wJalr...
104
104
  # AWS Region where your S3 buckets are located (e.g., us-east-1)
105
105
  AWS_REGION=us-east-1
106
106
 
107
+
108
+ # ========================================
109
+ # Synology (Optional)
110
+ # ========================================
111
+ # Synology DSM Host URL
112
+ SYNOLOGY_HOST=http://synology:5000
113
+
114
+ # Synology Username
115
+ SYNOLOGY_USERNAME=admin
116
+
117
+ # Synology Password
118
+ SYNOLOGY_PASSWORD=secret
119
+
107
120
  # ========================================
108
121
  # NextBank API (Placeholder)
109
122
  # ========================================
@@ -110,6 +110,19 @@ AWS_ACCESS_SECRET_KEY=wJalr...
110
110
  # AWS Region (e.g., us-east-1, ap-southeast-1)
111
111
  AWS_REGION=us-east-1
112
112
 
113
+ # ============================================
114
+ # Synology (Optional)
115
+ # ============================================
116
+
117
+ # Synology DSM Host URL
118
+ SYNOLOGY_HOST=http://synology:5000
119
+
120
+ # Synology Username
121
+ SYNOLOGY_USERNAME=admin
122
+
123
+ # Synology Password
124
+ SYNOLOGY_PASSWORD=secret
125
+
113
126
  # ============================================
114
127
  # NextBank (Placeholder - Optional)
115
128
  # ============================================
@@ -470,6 +483,37 @@ AWS_REGION=us-east-1
470
483
 
471
484
  ---
472
485
 
486
+ ### Synology Variables (Optional)
487
+
488
+ #### SYNOLOGY_HOST
489
+
490
+ **Purpose:** URL of your Synology DSM
491
+
492
+ **Example:**
493
+ ```bash
494
+ SYNOLOGY_HOST=http://192.168.1.100:5000
495
+ ```
496
+
497
+ #### SYNOLOGY_USERNAME
498
+
499
+ **Purpose:** Username for Synology authentication
500
+
501
+ **Example:**
502
+ ```bash
503
+ SYNOLOGY_USERNAME=admin
504
+ ```
505
+
506
+ #### SYNOLOGY_PASSWORD
507
+
508
+ **Purpose:** Password for Synology authentication
509
+
510
+ **Example:**
511
+ ```bash
512
+ SYNOLOGY_PASSWORD=secret
513
+ ```
514
+
515
+ ---
516
+
473
517
  ### NextBank Variables (Placeholder)
474
518
 
475
519
  **Note:** NextBank is a placeholder integration. These variables are optional and not currently used.
@@ -0,0 +1,78 @@
1
+ # Reusable Integrations
2
+
3
+ The DTT Framework provides a unified interface to access all configured integrations through a single `framework` instance. This allows you to easily use third-party services in your application logic without manually managing connections or clients.
4
+
5
+ ## Usage
6
+
7
+ You can import the `framework` instance from `@/lib/framework`.
8
+
9
+ ```typescript
10
+ import { framework } from '@/lib/framework'
11
+
12
+ async function myFunction() {
13
+ // 1. Clerk Authentication
14
+ const user = await framework.clerk.users.getUser('user_id')
15
+
16
+ // 2. Supabase (Admin Access)
17
+ const { data } = await framework.supabase
18
+ .from('my_table')
19
+ .select('*')
20
+
21
+ // 3. Snowflake Query
22
+ const rows = await framework.snowflake.query('SELECT * FROM PUBLIC.USERS LIMIT 10')
23
+
24
+ // 4. NextBank API
25
+ const account = await framework.nextbank.get('/accounts/123')
26
+ await framework.nextbank.post('/transfers', { amount: 100 })
27
+
28
+ // 5. Synology File Operations
29
+ const files = await framework.synology.listFiles('/home/user')
30
+ const fileContent = await framework.synology.getFile('/home/user/doc.pdf')
31
+
32
+ // 6. AWS S3
33
+ await framework.s3.uploadFile('my-bucket', 'uploads/hello.txt', 'Hello World')
34
+ }
35
+ ```
36
+
37
+ ## Integration Details
38
+
39
+ ### Clerk (`framework.clerk`)
40
+ Exposes the `@clerk/nextjs/server` client. Use this for server-side user management, organization handling, and session verification.
41
+
42
+ ### Supabase (`framework.supabase`)
43
+ Exposes the Supabase Admin client (using `SUPABASE_SERVICE_ROLE_KEY`).
44
+ **Note:** This client has full admin privileges. Be careful when using it in client-facing code (it should only be used server-side).
45
+
46
+ ### Snowflake (`framework.snowflake`)
47
+ Provides a simplified `query` method that handles connection pooling, execution, and cleanup automatically.
48
+ - `query(sqlText, binds?)`: Executes a SQL query.
49
+
50
+ ### NextBank (`framework.nextbank`)
51
+ A wrapper around the NextBank API that automatically handles:
52
+ - Base URL injection
53
+ - Authentication headers
54
+ - JSON parsing
55
+ - Error handling
56
+
57
+ Methods:
58
+ - `get(endpoint)`
59
+ - `post(endpoint, body)`
60
+ - `put(endpoint, body)`
61
+ - `delete(endpoint)`
62
+
63
+ ### Synology (`framework.synology`)
64
+ Provides methods to interact with Synology File Station.
65
+ - `listFiles(folderPath)`
66
+ - `getFile(filePath)`
67
+ - `deleteFile(filePath)`
68
+
69
+ ### AWS S3 (`framework.s3`)
70
+ Wraps the AWS SDK S3 Client.
71
+ - `listBuckets()`
72
+ - `listFiles(bucket, prefix?)`
73
+ - `uploadFile(bucket, key, body, contentType?)`
74
+ - `getFile(bucket, key)`
75
+ - `deleteFile(bucket, key)`
76
+ - `getPresignedUrl(bucket, key, expiresIn?)`
77
+ - `getClient()`: Returns the raw AWS S3 Client for advanced operations.
78
+
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odvi/dtt-framework",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -64,6 +64,7 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@aws-sdk/client-s3": "^3.958.0",
67
+ "@aws-sdk/s3-request-presigner": "^3.958.0",
67
68
  "@clerk/nextjs": "^6.36.5",
68
69
  "@radix-ui/react-collapsible": "^1.1.12",
69
70
  "@radix-ui/react-dialog": "^1.1.15",
@@ -80,6 +81,7 @@
80
81
  "gray-matter": "^4.0.3",
81
82
  "highlight.js": "^11.11.1",
82
83
  "hono": "^4.11.3",
84
+ "https-proxy-agent": "^7.0.6",
83
85
  "lucide-react": "^0.562.0",
84
86
  "next": "^15.2.3",
85
87
  "postgres": "^3.4.4",
@@ -35,6 +35,11 @@ const envSchema = z.object({
35
35
  AWS_ACCESS_KEY_ID: z.string().optional(),
36
36
  AWS_ACCESS_SECRET_KEY: z.string().optional(),
37
37
  AWS_REGION: z.string().optional(),
38
+
39
+ // Synology (optional)
40
+ SYNOLOGY_HOST: z.string().url().optional(),
41
+ SYNOLOGY_USERNAME: z.string().optional(),
42
+ SYNOLOGY_PASSWORD: z.string().optional(),
38
43
  })
39
44
 
40
45
  export const env = envSchema.parse(process.env)
@@ -52,6 +52,14 @@ export const SERVICES = [
52
52
  { name: 'Ping API', endpoint: '/nextbank/ping' },
53
53
  ],
54
54
  },
55
+ {
56
+ name: 'Synology',
57
+ icon: 'server',
58
+ checks: [
59
+ { name: 'Ping Host', endpoint: '/synology/ping' },
60
+ { name: 'Authentication', endpoint: '/synology/auth' },
61
+ ],
62
+ },
55
63
  {
56
64
  name: 'AWS S3',
57
65
  icon: 'cloud',
@@ -0,0 +1,103 @@
1
+ import {
2
+ S3Client,
3
+ ListBucketsCommand,
4
+ PutObjectCommand,
5
+ GetObjectCommand,
6
+ DeleteObjectCommand,
7
+ ListObjectsV2Command
8
+ } from '@aws-sdk/client-s3'
9
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
10
+ import { env } from '@/config/env'
11
+
12
+ export class S3IntegrationClient {
13
+ private client: S3Client
14
+ private region: string
15
+
16
+ constructor() {
17
+ this.region = env.AWS_REGION ?? 'us-east-1'
18
+
19
+ // Initialize with credentials from env
20
+ this.client = new S3Client({
21
+ region: this.region,
22
+ credentials: {
23
+ accessKeyId: env.AWS_ACCESS_KEY_ID ?? '',
24
+ secretAccessKey: env.AWS_ACCESS_SECRET_KEY ?? '',
25
+ },
26
+ })
27
+ }
28
+
29
+ /**
30
+ * Get the underlying S3 Client
31
+ */
32
+ getClient(): S3Client {
33
+ return this.client
34
+ }
35
+
36
+ /**
37
+ * List all buckets
38
+ */
39
+ async listBuckets() {
40
+ const command = new ListBucketsCommand({})
41
+ return this.client.send(command)
42
+ }
43
+
44
+ /**
45
+ * List files in a bucket
46
+ */
47
+ async listFiles(bucket: string, prefix?: string) {
48
+ const command = new ListObjectsV2Command({
49
+ Bucket: bucket,
50
+ Prefix: prefix,
51
+ })
52
+ return this.client.send(command)
53
+ }
54
+
55
+ /**
56
+ * Upload a file to S3
57
+ */
58
+ async uploadFile(bucket: string, key: string, body: Buffer | Uint8Array | Blob | string, contentType?: string) {
59
+ const command = new PutObjectCommand({
60
+ Bucket: bucket,
61
+ Key: key,
62
+ Body: body,
63
+ ContentType: contentType,
64
+ })
65
+ return this.client.send(command)
66
+ }
67
+
68
+ /**
69
+ * Get a file from S3 (returns the raw output)
70
+ */
71
+ async getFile(bucket: string, key: string) {
72
+ const command = new GetObjectCommand({
73
+ Bucket: bucket,
74
+ Key: key,
75
+ })
76
+ return this.client.send(command)
77
+ }
78
+
79
+ /**
80
+ * Generate a presigned URL for a file
81
+ */
82
+ async getPresignedUrl(bucket: string, key: string, expiresIn = 3600) {
83
+ const command = new GetObjectCommand({
84
+ Bucket: bucket,
85
+ Key: key,
86
+ })
87
+ return getSignedUrl(this.client, command, { expiresIn })
88
+ }
89
+
90
+ /**
91
+ * Delete a file from S3
92
+ */
93
+ async deleteFile(bucket: string, key: string) {
94
+ const command = new DeleteObjectCommand({
95
+ Bucket: bucket,
96
+ Key: key,
97
+ })
98
+ return this.client.send(command)
99
+ }
100
+ }
101
+
102
+ export const s3Client = new S3IntegrationClient()
103
+
@@ -0,0 +1,47 @@
1
+ import { clerkClient } from '@clerk/nextjs/server'
2
+ import { supabaseAdmin } from './supabase/admin'
3
+ import { nextbankClient } from './nextbank/client'
4
+ import { synologyClient } from './synology/client'
5
+ import { snowflakeClient } from './snowflake/client'
6
+ import { s3Client } from './aws-s3/client'
7
+
8
+ export class Framework {
9
+ /**
10
+ * Clerk Authentication SDK
11
+ * Use this to manage users, sessions, and organizations.
12
+ */
13
+ public clerk = clerkClient
14
+
15
+ /**
16
+ * Supabase Admin Client (Service Role)
17
+ * Use this for database and storage operations with full privileges.
18
+ */
19
+ public supabase = supabaseAdmin
20
+
21
+ /**
22
+ * Snowflake Client
23
+ * Use this to execute SQL queries against your Snowflake data warehouse.
24
+ */
25
+ public snowflake = snowflakeClient
26
+
27
+ /**
28
+ * AWS S3 Client
29
+ * Use this to manage files in AWS S3 buckets.
30
+ */
31
+ public s3 = s3Client
32
+
33
+ /**
34
+ * NextBank Integration
35
+ * Use this to interact with the NextBank API.
36
+ */
37
+ public nextbank = nextbankClient
38
+
39
+ /**
40
+ * Synology Integration
41
+ * Use this to manage files on a Synology NAS.
42
+ */
43
+ public synology = synologyClient
44
+ }
45
+
46
+ export const framework = new Framework()
47
+
@@ -17,22 +17,21 @@ class NextBankClient {
17
17
  return { Authorization: `Basic ${token}` }
18
18
  }
19
19
 
20
- async ping(fingerprint: string): Promise<{ status: string; timestamp: string }> {
21
- const url = `${this.apiUrl}/management/status`
22
- console.log(`[NextBank] Pinging ${url}`)
20
+ private async request<T = any>(endpoint: string, options: RequestInit = {}): Promise<T> {
21
+ const url = `${this.apiUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`
23
22
 
23
+ const headers = {
24
+ ...this.getAuthHeader(),
25
+ 'Content-Type': 'application/json',
26
+ 'Accept': 'application/json',
27
+ 'User-Agent': 'dtt-framework-client',
28
+ ...options.headers,
29
+ }
30
+
24
31
  try {
25
32
  const response = await fetch(url, {
26
- method: 'POST',
27
- headers: {
28
- ...this.getAuthHeader(),
29
- 'Content-Type': 'application/json',
30
- 'Accept': 'application/json',
31
- 'User-Agent': 'dtt-framework-health-check',
32
- },
33
- body: JSON.stringify({
34
- fingerprint: fingerprint,
35
- }),
33
+ ...options,
34
+ headers,
36
35
  })
37
36
 
38
37
  const contentType = response.headers.get('content-type')
@@ -40,28 +39,54 @@ class NextBankClient {
40
39
  if (!response.ok) {
41
40
  if (contentType && contentType.includes('text/html')) {
42
41
  const text = await response.text()
43
- console.error('[NextBank] Received HTML error response. Possible causes:')
44
- console.error('1. NEXTBANK_API is pointing to the wrong server (e.g. Next.js app instead of NextBank API)')
45
- console.error('2. The endpoint /management/status does not exist or does not support POST')
46
- console.error(`[NextBank] Response preview: ${text.substring(0, 150)}...`)
47
- throw new Error(`NextBank API error: ${response.status} (HTML response)`)
42
+ throw new Error(`NextBank API error: ${response.status} (HTML response): ${text.substring(0, 100)}...`)
48
43
  }
49
44
  throw new Error(`NextBank API error: ${response.status} ${response.statusText}`)
50
45
  }
51
46
 
52
47
  if (contentType && contentType.includes('application/json')) {
53
48
  return await response.json()
54
- } else {
55
- const text = await response.text()
56
- console.error('[NextBank] Invalid response content type:', contentType)
57
- console.error('[NextBank] Response preview:', text.substring(0, 200))
58
- throw new Error(`Invalid response format. Expected JSON, got ${contentType}`)
49
+ }
50
+
51
+ // If not JSON, return text if generic T is string, or try to return as is?
52
+ // For now, assume JSON or void.
53
+ const text = await response.text()
54
+ try {
55
+ return JSON.parse(text)
56
+ } catch {
57
+ return text as unknown as T
59
58
  }
60
59
  } catch (error) {
61
- console.error('[NextBank] Ping failed:', error)
60
+ console.error(`[NextBank] Request to ${endpoint} failed:`, error)
62
61
  throw error
63
62
  }
64
63
  }
64
+
65
+ async get<T = any>(endpoint: string): Promise<T> {
66
+ return this.request<T>(endpoint, { method: 'GET' })
67
+ }
68
+
69
+ async post<T = any>(endpoint: string, body: any): Promise<T> {
70
+ return this.request<T>(endpoint, {
71
+ method: 'POST',
72
+ body: JSON.stringify(body)
73
+ })
74
+ }
75
+
76
+ async put<T = any>(endpoint: string, body: any): Promise<T> {
77
+ return this.request<T>(endpoint, {
78
+ method: 'PUT',
79
+ body: JSON.stringify(body)
80
+ })
81
+ }
82
+
83
+ async delete<T = any>(endpoint: string): Promise<T> {
84
+ return this.request<T>(endpoint, { method: 'DELETE' })
85
+ }
86
+
87
+ async ping(fingerprint: string): Promise<{ status: string; timestamp: string }> {
88
+ return this.post('/management/status', { fingerprint })
89
+ }
65
90
  }
66
91
 
67
92
  export const nextbankClient = new NextBankClient()
@@ -100,3 +100,40 @@ export async function destroyConnection(connection: Connection): Promise<void> {
100
100
  })
101
101
  })
102
102
  }
103
+
104
+ export class SnowflakeClient {
105
+ async query<T = unknown>(sqlText: string, binds?: Bind[]): Promise<T[]> {
106
+ let connection: Connection | undefined
107
+ try {
108
+ connection = await createSnowflakeConnection()
109
+
110
+ // Connect
111
+ await new Promise<void>((resolve, reject) => {
112
+ connection!.connect((err) => {
113
+ if (err) reject(err)
114
+ else resolve()
115
+ })
116
+ })
117
+
118
+ // Execute
119
+ return await new Promise<T[]>((resolve, reject) => {
120
+ connection!.execute({
121
+ sqlText,
122
+ binds: binds as any,
123
+ complete: (err, stmt, rows) => {
124
+ if (err) reject(err)
125
+ else resolve((rows || []) as T[])
126
+ },
127
+ })
128
+ })
129
+ } finally {
130
+ if (connection) {
131
+ connection.destroy((err) => {
132
+ if (err) console.error('Error destroying Snowflake connection:', err)
133
+ })
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ export const snowflakeClient = new SnowflakeClient()
@@ -0,0 +1,239 @@
1
+ import { env } from '@/config/env'
2
+ import https from 'https'
3
+
4
+ class SynologyClient {
5
+ private host: string
6
+ private username: string
7
+ private password: string
8
+ private sessionID: string | null = null
9
+
10
+ constructor() {
11
+ this.host = env.SYNOLOGY_HOST ?? ''
12
+ this.username = env.SYNOLOGY_USERNAME ?? ''
13
+ this.password = env.SYNOLOGY_PASSWORD ?? ''
14
+ }
15
+
16
+ // Helper to perform fetch with custom agent for self-signed certs
17
+ private async fetchWithAgent(url: string, options: RequestInit = {}) {
18
+ // For local development with self-signed certs, especially with Next.js/Node fetch,
19
+ // explicitly ignoring certificate errors at the process level for this specific request
20
+ // context is sometimes the most reliable way if agents aren't being picked up.
21
+
22
+ // Create an agent that ignores self-signed certificate errors
23
+ const agent = new https.Agent({
24
+ rejectUnauthorized: false
25
+ });
26
+
27
+ // Note: In Next.js App Router (server components/actions), fetch is patched.
28
+ // Standard https.Agent might not work as expected with the patched fetch.
29
+ // However, for API routes (Pages router) or Route Handlers (App router), it runs in Node environment.
30
+ // If the error persists, it's often because the fetch implementation doesn't support the 'agent' option directly
31
+ // or is using Undici which needs a Dispatcher.
32
+
33
+ // Let's try a more robust approach:
34
+ // 1. Pass 'agent' for node-fetch compatibility
35
+ // 2. Pass 'dispatcher' for Undici compatibility (Node 18+ native fetch)
36
+ // 3. Set 'cache: no-store' to ensure we aren't hitting cached responses
37
+
38
+ // @ts-ignore
39
+ const fetchOptions: any = {
40
+ ...options,
41
+ cache: 'no-store',
42
+ // 'agent' is for node-fetch / older polyfills
43
+ agent: agent,
44
+ };
45
+
46
+ // Attempt to handle Undici (Node 18+ native fetch) which Next.js uses
47
+ // We can't easily import 'Agent' from undici without adding it as dependency,
48
+ // but we can try to rely on the fact that sometimes just the node-fetch 'agent' isn't enough.
49
+
50
+ try {
51
+ return await fetch(url, fetchOptions);
52
+ } catch (err: any) {
53
+ // If fetch fails with a certificate error, it might be because 'agent' was ignored.
54
+ // As a fallback for development environments, we can temporarily disable TLS check
55
+ // WARNING: This is dangerous in production, but acceptable for "Synology local IP" scenarios
56
+ // if strictly scoped or if the user explicitly configured it.
57
+
58
+ if (err.message?.includes('certificate') || err.cause?.message?.includes('certificate') || err.cause?.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
59
+ console.warn('[Synology] Certificate error detected. Retrying with permissive fetch...');
60
+
61
+ // This is a hacky workaround for environments where Agent isn't working for fetch
62
+ const originalReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
63
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
64
+
65
+ try {
66
+ const retryResponse = await fetch(url, { ...options, cache: 'no-store' });
67
+ return retryResponse;
68
+ } finally {
69
+ // Restore original setting immediately
70
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalReject;
71
+ }
72
+ }
73
+ throw err;
74
+ }
75
+ }
76
+
77
+ async listFiles(folderPath: string) {
78
+ if (!this.sessionID) await this.login()
79
+
80
+ const params = new URLSearchParams({
81
+ api: 'SYNO.FileStation.List',
82
+ version: '2',
83
+ method: 'list',
84
+ folder_path: folderPath,
85
+ _sid: this.sessionID!
86
+ })
87
+
88
+ const url = `${this.host}/webapi/entry.cgi?${params.toString()}`
89
+ const response = await this.fetchWithAgent(url)
90
+
91
+ if (!response.ok) {
92
+ throw new Error(`Synology ListFiles error: ${response.status}`)
93
+ }
94
+
95
+ const data = await response.json()
96
+ if (!data.success) {
97
+ throw new Error(`Synology ListFiles failed: ${JSON.stringify(data)}`)
98
+ }
99
+ return data.data.files
100
+ }
101
+
102
+ async getFile(filePath: string) {
103
+ if (!this.sessionID) await this.login()
104
+
105
+ const params = new URLSearchParams({
106
+ api: 'SYNO.FileStation.Download',
107
+ version: '2',
108
+ method: 'download',
109
+ path: filePath,
110
+ mode: 'open',
111
+ _sid: this.sessionID!
112
+ })
113
+
114
+ const url = `${this.host}/webapi/entry.cgi?${params.toString()}`
115
+ const response = await this.fetchWithAgent(url)
116
+
117
+ if (!response.ok) {
118
+ throw new Error(`Synology GetFile error: ${response.status}`)
119
+ }
120
+
121
+ // Return array buffer or blob?
122
+ return response.arrayBuffer()
123
+ }
124
+
125
+ async deleteFile(filePath: string) {
126
+ if (!this.sessionID) await this.login()
127
+
128
+ const params = new URLSearchParams({
129
+ api: 'SYNO.FileStation.Delete',
130
+ version: '2',
131
+ method: 'delete',
132
+ path: filePath,
133
+ _sid: this.sessionID!
134
+ })
135
+
136
+ const url = `${this.host}/webapi/entry.cgi?${params.toString()}`
137
+ const response = await this.fetchWithAgent(url)
138
+
139
+ if (!response.ok) {
140
+ throw new Error(`Synology DeleteFile error: ${response.status}`)
141
+ }
142
+
143
+ const data = await response.json()
144
+ if (!data.success) {
145
+ throw new Error(`Synology DeleteFile failed: ${JSON.stringify(data)}`)
146
+ }
147
+ return data
148
+ }
149
+
150
+ async ping(): Promise<{ status: string; timestamp: string; version?: string; sid?: string }> {
151
+ if (!this.host) {
152
+ throw new Error('Synology host not configured')
153
+ }
154
+
155
+ const params = new URLSearchParams({
156
+ api: 'SYNO.API.Info',
157
+ version: '1',
158
+ method: 'query',
159
+ query: 'SYNO.API.Auth'
160
+ })
161
+
162
+ const infoUrl = `${this.host}/webapi/query.cgi?${params.toString()}`
163
+ console.log(`[Synology] Pinging ${infoUrl}`)
164
+
165
+ try {
166
+ const response = await this.fetchWithAgent(infoUrl)
167
+
168
+ if (!response.ok) {
169
+ throw new Error(`Synology API error: ${response.status} ${response.statusText}`)
170
+ }
171
+
172
+ const data = await response.json()
173
+
174
+ if (!data.success) {
175
+ throw new Error(`Synology API returned error: ${JSON.stringify(data)}`)
176
+ }
177
+
178
+ let sid: string | undefined
179
+
180
+ // If we have credentials, try to login to verify them
181
+ if (this.username && this.password) {
182
+ sid = await this.login()
183
+ }
184
+
185
+ return {
186
+ status: 'healthy',
187
+ timestamp: new Date().toISOString(),
188
+ version: data.data?.['SYNO.API.Auth']?.maxVersion?.toString(),
189
+ sid
190
+ }
191
+ } catch (error) {
192
+ console.error('[Synology] Ping failed:', error)
193
+ throw error
194
+ }
195
+ }
196
+
197
+ private async login(): Promise<string> {
198
+ const loginUrl = `${this.host}/webapi/auth.cgi`
199
+
200
+ const params = new URLSearchParams({
201
+ api: 'SYNO.API.Auth',
202
+ version: '6',
203
+ method: 'login',
204
+ account: this.username,
205
+ passwd: this.password,
206
+ session: 'FileStation',
207
+ format: 'sid'
208
+ })
209
+
210
+ console.log(`[Synology] Logging in...`)
211
+
212
+ try {
213
+ const response = await this.fetchWithAgent(`${loginUrl}?${params.toString()}`)
214
+
215
+ if (!response.ok) {
216
+ throw new Error(`Synology Auth error: ${response.status} ${response.statusText}`)
217
+ }
218
+
219
+ const data = await response.json()
220
+
221
+ if (!data.success) {
222
+ throw new Error(`Synology Auth failed: ${JSON.stringify(data)}`)
223
+ }
224
+
225
+ if (data.data?.sid) {
226
+ this.sessionID = data.data.sid
227
+ return this.sessionID!
228
+ }
229
+
230
+ throw new Error('No SID in response')
231
+ } catch (error) {
232
+ console.error('[Synology] Login error:', error)
233
+ this.sessionID = null
234
+ throw error
235
+ }
236
+ }
237
+ }
238
+
239
+ export const synologyClient = new SynologyClient()
@@ -5,6 +5,7 @@ import { storageHealthRoutes } from './storage'
5
5
  import { edgeFunctionsHealthRoutes } from './edge-functions'
6
6
  import { snowflakeHealthRoutes } from './snowflake'
7
7
  import { nextbankHealthRoutes } from './nextbank'
8
+ import { synologyHealthRoutes } from './synology'
8
9
  import { frameworkHealthRoutes } from './framework'
9
10
  import { awsS3HealthRoutes } from './aws-s3'
10
11
 
@@ -16,6 +17,7 @@ healthRoutes.route('/storage', storageHealthRoutes)
16
17
  healthRoutes.route('/edge', edgeFunctionsHealthRoutes)
17
18
  healthRoutes.route('/snowflake', snowflakeHealthRoutes)
18
19
  healthRoutes.route('/nextbank', nextbankHealthRoutes)
20
+ healthRoutes.route('/synology', synologyHealthRoutes)
19
21
  healthRoutes.route('/framework', frameworkHealthRoutes)
20
22
  healthRoutes.route('/aws-s3', awsS3HealthRoutes)
21
23
 
@@ -40,6 +42,8 @@ healthRoutes.get('/all', async (c) => {
40
42
  { name: 'Snowflake Query', url: `${baseUrl}/api/health/snowflake/query` },
41
43
  { name: 'NextBank Ping', url: `${baseUrl}/api/health/nextbank/ping` },
42
44
  { name: 'NextBank Auth', url: `${baseUrl}/api/health/nextbank/auth` },
45
+ { name: 'Synology Ping', url: `${baseUrl}/api/health/synology/ping` },
46
+ { name: 'Synology Auth', url: `${baseUrl}/api/health/synology/auth` },
43
47
  { name: 'AWS S3 Connect', url: `${baseUrl}/api/health/aws-s3/connect` },
44
48
  ]
45
49
 
@@ -0,0 +1,69 @@
1
+ import { Hono } from 'hono'
2
+ import { synologyClient } from '@/lib/synology/client'
3
+ import { env } from '@/config/env'
4
+
5
+ export const synologyHealthRoutes = new Hono()
6
+
7
+ synologyHealthRoutes.get('/ping', async (c) => {
8
+ const start = performance.now()
9
+
10
+ if (!env.SYNOLOGY_HOST) {
11
+ return c.json({
12
+ status: 'unconfigured',
13
+ responseTimeMs: Math.round(performance.now() - start),
14
+ message: 'Synology Host not configured',
15
+ })
16
+ }
17
+
18
+ try {
19
+ const result = await synologyClient.ping()
20
+ return c.json({
21
+ status: 'healthy',
22
+ responseTimeMs: Math.round(performance.now() - start),
23
+ message: 'Successfully connected to Synology',
24
+ data: result,
25
+ })
26
+ } catch (error) {
27
+ return c.json(
28
+ {
29
+ status: 'error',
30
+ responseTimeMs: Math.round(performance.now() - start),
31
+ error: error instanceof Error ? error.message : 'Ping failed',
32
+ },
33
+ 500
34
+ )
35
+ }
36
+ })
37
+
38
+ synologyHealthRoutes.get('/auth', async (c) => {
39
+ const start = performance.now()
40
+
41
+ if (!env.SYNOLOGY_HOST || !env.SYNOLOGY_USERNAME || !env.SYNOLOGY_PASSWORD) {
42
+ return c.json({
43
+ status: 'unconfigured',
44
+ responseTimeMs: Math.round(performance.now() - start),
45
+ message: 'Synology credentials not configured',
46
+ })
47
+ }
48
+
49
+ try {
50
+ // The ping method already does a login check if credentials are provided
51
+ const result = await synologyClient.ping()
52
+ return c.json({
53
+ status: 'healthy',
54
+ responseTimeMs: Math.round(performance.now() - start),
55
+ message: 'Successfully authenticated with Synology',
56
+ data: result,
57
+ })
58
+ } catch (error) {
59
+ return c.json(
60
+ {
61
+ status: 'error',
62
+ responseTimeMs: Math.round(performance.now() - start),
63
+ error: error instanceof Error ? error.message : 'Auth failed',
64
+ },
65
+ 500
66
+ )
67
+ }
68
+ })
69
+