@odvi/create-dtt-framework 0.1.12 → 0.1.13
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 +1 -1
- package/template/.env.example +13 -0
- package/template/docs/framework/environment-variables.md +44 -0
- package/template/package.json +2 -1
- package/template/src/config/env.ts +5 -0
- package/template/src/features/health-check/config.ts +8 -0
- package/template/src/lib/synology/client.ts +166 -0
- package/template/src/server/api/routes/health/index.ts +4 -0
- package/template/src/server/api/routes/health/synology.ts +69 -0
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -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.
|
package/template/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odvi/dtt-framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"gray-matter": "^4.0.3",
|
|
81
81
|
"highlight.js": "^11.11.1",
|
|
82
82
|
"hono": "^4.11.3",
|
|
83
|
+
"https-proxy-agent": "^7.0.6",
|
|
83
84
|
"lucide-react": "^0.562.0",
|
|
84
85
|
"next": "^15.2.3",
|
|
85
86
|
"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,166 @@
|
|
|
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 ping(): Promise<{ status: string; timestamp: string; version?: string; sid?: string }> {
|
|
78
|
+
if (!this.host) {
|
|
79
|
+
throw new Error('Synology host not configured')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const params = new URLSearchParams({
|
|
83
|
+
api: 'SYNO.API.Info',
|
|
84
|
+
version: '1',
|
|
85
|
+
method: 'query',
|
|
86
|
+
query: 'SYNO.API.Auth'
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const infoUrl = `${this.host}/webapi/query.cgi?${params.toString()}`
|
|
90
|
+
console.log(`[Synology] Pinging ${infoUrl}`)
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const response = await this.fetchWithAgent(infoUrl)
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(`Synology API error: ${response.status} ${response.statusText}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const data = await response.json()
|
|
100
|
+
|
|
101
|
+
if (!data.success) {
|
|
102
|
+
throw new Error(`Synology API returned error: ${JSON.stringify(data)}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let sid: string | undefined
|
|
106
|
+
|
|
107
|
+
// If we have credentials, try to login to verify them
|
|
108
|
+
if (this.username && this.password) {
|
|
109
|
+
sid = await this.login()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
status: 'healthy',
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
version: data.data?.['SYNO.API.Auth']?.maxVersion?.toString(),
|
|
116
|
+
sid
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('[Synology] Ping failed:', error)
|
|
120
|
+
throw error
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async login(): Promise<string> {
|
|
125
|
+
const loginUrl = `${this.host}/webapi/auth.cgi`
|
|
126
|
+
|
|
127
|
+
const params = new URLSearchParams({
|
|
128
|
+
api: 'SYNO.API.Auth',
|
|
129
|
+
version: '6',
|
|
130
|
+
method: 'login',
|
|
131
|
+
account: this.username,
|
|
132
|
+
passwd: this.password,
|
|
133
|
+
session: 'FileStation',
|
|
134
|
+
format: 'sid'
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
console.log(`[Synology] Logging in...`)
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const response = await this.fetchWithAgent(`${loginUrl}?${params.toString()}`)
|
|
141
|
+
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
throw new Error(`Synology Auth error: ${response.status} ${response.statusText}`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const data = await response.json()
|
|
147
|
+
|
|
148
|
+
if (!data.success) {
|
|
149
|
+
throw new Error(`Synology Auth failed: ${JSON.stringify(data)}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (data.data?.sid) {
|
|
153
|
+
this.sessionID = data.data.sid
|
|
154
|
+
return this.sessionID!
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new Error('No SID in response')
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('[Synology] Login error:', error)
|
|
160
|
+
this.sessionID = null
|
|
161
|
+
throw error
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
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
|
+
|