@sentroy-co/client-sdk 2.6.4 → 2.9.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/AGENTS.md +138 -2
- package/README.md +44 -1
- package/bin/sentroy.js +4 -0
- package/dist/cli/dotenv.d.ts +35 -0
- package/dist/cli/dotenv.d.ts.map +1 -0
- package/dist/cli/dotenv.js +125 -0
- package/dist/cli/dotenv.js.map +1 -0
- package/dist/cli/env.d.ts +11 -0
- package/dist/cli/env.d.ts.map +1 -0
- package/dist/cli/env.js +331 -0
- package/dist/cli/env.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +105 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/vault/index.d.ts +73 -0
- package/dist/vault/index.d.ts.map +1 -0
- package/dist/vault/index.js +169 -0
- package/dist/vault/index.js.map +1 -0
- package/dist/vault/react.d.ts +27 -0
- package/dist/vault/react.d.ts.map +1 -0
- package/dist/vault/react.js +73 -0
- package/dist/vault/react.js.map +1 -0
- package/package.json +23 -3
- package/src/cli/dotenv.ts +146 -0
- package/src/cli/env.ts +402 -0
- package/src/cli/index.ts +122 -0
- package/src/vault/index.ts +198 -0
- package/src/vault/react.tsx +125 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sentroy-co/client-sdk/vault` — Sentroy Env Vault server-side client.
|
|
3
|
+
*
|
|
4
|
+
* Bootstrap pattern:
|
|
5
|
+
* `SENTROY_ENV_API_KEY` (process.env) → tek dış env. Fonksiyonlar
|
|
6
|
+
* `getEnv("KEY")` ya da `getEnvOrThrow("KEY")` çağrılırken in-memory
|
|
7
|
+
* cache'den döner; ilk çağrıda Sentroy core'a HTTP fetch yapar ve
|
|
8
|
+
* o token scope'undaki TÜM env'leri (public + private) çeker.
|
|
9
|
+
*
|
|
10
|
+
* Cache stratejisi:
|
|
11
|
+
* • Default TTL 5 dk; refresh deadline aşıldığında bir sonraki
|
|
12
|
+
* `getEnv` çağrısında re-fetch tetiklenir.
|
|
13
|
+
* • `await refreshEnvCache()` manuel invalidation — webhook ya da
|
|
14
|
+
* SIGHUP-style restart sinyaline bağlanabilir.
|
|
15
|
+
* • `setEnvCacheTTL(seconds)` runtime'da TTL değiştirme.
|
|
16
|
+
*
|
|
17
|
+
* Hata politikası: bootstrap fail (token yok / network down / 401)
|
|
18
|
+
* → `getEnv` her çağrıda undefined döner; `getEnvOrThrow` exception
|
|
19
|
+
* atar. Process startup'ında `await preloadEnv()` çağırırsanız
|
|
20
|
+
* eksik env'leri erkenden yakalarsınız.
|
|
21
|
+
*
|
|
22
|
+
* **NOT**: Bu modül `Sentroy` ana client'ından (mail/storage REST
|
|
23
|
+
* resource'ları) bağımsızdır. Vault token'ları (stk_env_*) ile mail/
|
|
24
|
+
* storage token'ları (stk_*) farklı namespace'tedir; tek client'ta
|
|
25
|
+
* birleştirmek ergonomiyi bozardı.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface EnvVariable {
|
|
29
|
+
key: string
|
|
30
|
+
value: string
|
|
31
|
+
type: string
|
|
32
|
+
public: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface EnvCacheState {
|
|
36
|
+
fetchedAt: number
|
|
37
|
+
variables: Map<string, EnvVariable>
|
|
38
|
+
project: string
|
|
39
|
+
environment: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000
|
|
43
|
+
const DEFAULT_BASE_URL = "https://sentroy.com"
|
|
44
|
+
|
|
45
|
+
interface ClientOptions {
|
|
46
|
+
/** Sentroy core URL (defaults to env or https://sentroy.com). */
|
|
47
|
+
baseUrl?: string
|
|
48
|
+
/** API key — defaults to `process.env.SENTROY_ENV_API_KEY`. */
|
|
49
|
+
apiKey?: string
|
|
50
|
+
/** Cache TTL in seconds; default 300. */
|
|
51
|
+
ttlSeconds?: number
|
|
52
|
+
/** Fetch timeout in ms; default 5000. */
|
|
53
|
+
timeoutMs?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let resolvedBaseUrl = DEFAULT_BASE_URL
|
|
57
|
+
let resolvedApiKey: string | undefined
|
|
58
|
+
let cacheTtlMs = DEFAULT_TTL_MS
|
|
59
|
+
let fetchTimeoutMs = 5000
|
|
60
|
+
let cache: EnvCacheState | null = null
|
|
61
|
+
let pendingRefresh: Promise<void> | null = null
|
|
62
|
+
|
|
63
|
+
function readEnv(name: string): string | undefined {
|
|
64
|
+
if (typeof process === "undefined") return undefined
|
|
65
|
+
return process.env?.[name]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* One-time client config — Sentroy app'lerinde modül seviyesinde çağrılır,
|
|
70
|
+
* default'lara güvenilirse hiç çağrılmasına gerek yok.
|
|
71
|
+
*/
|
|
72
|
+
export function configureEnvClient(options: ClientOptions = {}): void {
|
|
73
|
+
if (options.baseUrl) resolvedBaseUrl = options.baseUrl.replace(/\/+$/, "")
|
|
74
|
+
else
|
|
75
|
+
resolvedBaseUrl = (
|
|
76
|
+
readEnv("NEXT_PUBLIC_SENTROY_ENV_API_URL") ||
|
|
77
|
+
readEnv("SENTROY_ENV_API_URL") ||
|
|
78
|
+
readEnv("NEXT_PUBLIC_CORE_APP_URL") ||
|
|
79
|
+
DEFAULT_BASE_URL
|
|
80
|
+
).replace(/\/+$/, "")
|
|
81
|
+
resolvedApiKey = options.apiKey ?? readEnv("SENTROY_ENV_API_KEY")
|
|
82
|
+
if (options.ttlSeconds) cacheTtlMs = options.ttlSeconds * 1000
|
|
83
|
+
if (options.timeoutMs) fetchTimeoutMs = options.timeoutMs
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** TTL'i runtime'da değiştir (örn. development için kısa, prod için uzun). */
|
|
87
|
+
export function setEnvCacheTTL(seconds: number): void {
|
|
88
|
+
cacheTtlMs = seconds * 1000
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Cache'i invalidate et — webhook ya da admin-driven manual refresh için. */
|
|
92
|
+
export async function refreshEnvCache(): Promise<void> {
|
|
93
|
+
cache = null
|
|
94
|
+
await ensureCache()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Process start'ında erkenden tetikle — eksik env'i fail-fast yakalar. */
|
|
98
|
+
export async function preloadEnv(): Promise<void> {
|
|
99
|
+
await ensureCache()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function fetchVariables(): Promise<EnvCacheState> {
|
|
103
|
+
if (!resolvedApiKey) {
|
|
104
|
+
// Lazy bootstrap — configureEnvClient çağrılmadıysa env'den oku.
|
|
105
|
+
configureEnvClient()
|
|
106
|
+
}
|
|
107
|
+
if (!resolvedApiKey) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
"@sentroy-co/client-sdk/vault: SENTROY_ENV_API_KEY is not set. " +
|
|
110
|
+
"Set it on the platform (Coolify env) or call configureEnvClient({ apiKey: ... }) at boot.",
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
const url = `${resolvedBaseUrl}/api/env-vault/fetch`
|
|
114
|
+
const res = await fetch(url, {
|
|
115
|
+
headers: { Authorization: `Bearer ${resolvedApiKey}` },
|
|
116
|
+
signal: AbortSignal.timeout(fetchTimeoutMs),
|
|
117
|
+
cache: "no-store",
|
|
118
|
+
})
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`env-vault fetch failed: ${res.status} ${res.statusText} (url=${url})`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
const json = (await res.json()) as {
|
|
125
|
+
data?: {
|
|
126
|
+
project: string
|
|
127
|
+
environment: string
|
|
128
|
+
variables: EnvVariable[]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!json.data) throw new Error("env-vault fetch: malformed response")
|
|
132
|
+
const map = new Map<string, EnvVariable>()
|
|
133
|
+
for (const v of json.data.variables) map.set(v.key, v)
|
|
134
|
+
return {
|
|
135
|
+
fetchedAt: Date.now(),
|
|
136
|
+
variables: map,
|
|
137
|
+
project: json.data.project,
|
|
138
|
+
environment: json.data.environment,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function ensureCache(): Promise<EnvCacheState> {
|
|
143
|
+
const now = Date.now()
|
|
144
|
+
if (cache && now - cache.fetchedAt < cacheTtlMs) return cache
|
|
145
|
+
if (pendingRefresh) {
|
|
146
|
+
await pendingRefresh
|
|
147
|
+
if (cache) return cache
|
|
148
|
+
}
|
|
149
|
+
pendingRefresh = (async () => {
|
|
150
|
+
try {
|
|
151
|
+
cache = await fetchVariables()
|
|
152
|
+
} finally {
|
|
153
|
+
pendingRefresh = null
|
|
154
|
+
}
|
|
155
|
+
})()
|
|
156
|
+
await pendingRefresh
|
|
157
|
+
if (!cache) throw new Error("env-vault: cache hydrate failed")
|
|
158
|
+
return cache
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Async — env yoksa undefined. Bu fonksiyon TÜM env'leri (server+public)
|
|
163
|
+
* gizler, çünkü `process.env` fallback yok; sadece vault'ta kayıtlı
|
|
164
|
+
* olanlar dönder. Token bootstrap fail ederse exception atar.
|
|
165
|
+
*/
|
|
166
|
+
export async function getEnv(key: string): Promise<string | undefined> {
|
|
167
|
+
const c = await ensureCache()
|
|
168
|
+
return c.variables.get(key)?.value
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Eksik env'i hemen patlatır — config-validation pattern'inde kullanışlı. */
|
|
172
|
+
export async function getEnvOrThrow(key: string): Promise<string> {
|
|
173
|
+
const v = await getEnv(key)
|
|
174
|
+
if (v === undefined) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`env-vault: required variable ${key} is not defined (project=${cache?.project ?? "?"}, env=${cache?.environment ?? "?"})`,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
return v
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Tüm env'leri map olarak döner (dump için kullanışlı). */
|
|
183
|
+
export async function getAllEnvs(): Promise<Record<string, string>> {
|
|
184
|
+
const c = await ensureCache()
|
|
185
|
+
const out: Record<string, string> = {}
|
|
186
|
+
for (const [k, v] of c.variables) out[k] = v.value
|
|
187
|
+
return out
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Sadece public (`public: true`) env'ler — SSR helper için. */
|
|
191
|
+
export async function getPublicEnvs(): Promise<Record<string, string>> {
|
|
192
|
+
const c = await ensureCache()
|
|
193
|
+
const out: Record<string, string> = {}
|
|
194
|
+
for (const [k, v] of c.variables) {
|
|
195
|
+
if (v.public) out[k] = v.value
|
|
196
|
+
}
|
|
197
|
+
return out
|
|
198
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useState,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `@sentroy-co/client-sdk/vault/react` — React provider + hook.
|
|
13
|
+
*
|
|
14
|
+
* Akış:
|
|
15
|
+
* 1. Server-side `getPublicEnvs()` çağrılıp result `<EnvProvider envs={...}>`
|
|
16
|
+
* ile root layout'a SSR sırasında inject edilir → ilk paint'te
|
|
17
|
+
* `useEnv()` doğru değeri döndürür (FOUC yok).
|
|
18
|
+
* 2. Client-side periyodik refresh (`/api/env-vault/public` endpoint'i,
|
|
19
|
+
* yalnızca public:true variable'lar) — admin değer değiştirince UI
|
|
20
|
+
* kullanıcıyı zorla refresh etmeden günceli alır. `refreshIntervalMs`
|
|
21
|
+
* 0 verilirse polling kapalı.
|
|
22
|
+
*
|
|
23
|
+
* **Güvenlik:** Server-side `getEnv()` private env'leri de döner; bu hook
|
|
24
|
+
* yalnızca PUBLIC env'leri client'a sızdırır. Provider'a server-only
|
|
25
|
+
* env geçmek konvansiyon ihlali — `getPublicEnvs()` filter'ını atlayıp
|
|
26
|
+
* `getAllEnvs()` geçerseniz secret leak'lersiniz.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
interface EnvContextValue {
|
|
30
|
+
envs: Record<string, string>
|
|
31
|
+
loading: boolean
|
|
32
|
+
refresh: () => Promise<void>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const EnvContext = createContext<EnvContextValue>({
|
|
36
|
+
envs: {},
|
|
37
|
+
loading: false,
|
|
38
|
+
refresh: async () => {},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
interface EnvProviderProps {
|
|
42
|
+
/** Server-side fetched public envs — SSR'da inject edilir. */
|
|
43
|
+
envs: Record<string, string>
|
|
44
|
+
/** Public refresh endpoint URL — default `/api/env-vault/public`. */
|
|
45
|
+
refreshUrl?: string
|
|
46
|
+
/** Bearer token — public endpoint için. Default `process.env.NEXT_PUBLIC_SENTROY_ENV_API_KEY`. */
|
|
47
|
+
apiKey?: string
|
|
48
|
+
/** Refresh interval ms; 0 ise polling kapalı. Default 5 dk. */
|
|
49
|
+
refreshIntervalMs?: number
|
|
50
|
+
children: ReactNode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1000
|
|
54
|
+
|
|
55
|
+
export function EnvProvider({
|
|
56
|
+
envs: initialEnvs,
|
|
57
|
+
refreshUrl = "/api/env-vault/public",
|
|
58
|
+
apiKey,
|
|
59
|
+
refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS,
|
|
60
|
+
children,
|
|
61
|
+
}: EnvProviderProps) {
|
|
62
|
+
const [envs, setEnvs] = useState<Record<string, string>>(initialEnvs)
|
|
63
|
+
const [loading, setLoading] = useState(false)
|
|
64
|
+
|
|
65
|
+
const effectiveKey =
|
|
66
|
+
apiKey ??
|
|
67
|
+
(typeof process !== "undefined"
|
|
68
|
+
? process.env?.NEXT_PUBLIC_SENTROY_ENV_API_KEY
|
|
69
|
+
: undefined)
|
|
70
|
+
|
|
71
|
+
async function refresh() {
|
|
72
|
+
if (!effectiveKey) return // bootstrap yoksa polling no-op
|
|
73
|
+
setLoading(true)
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(refreshUrl, {
|
|
76
|
+
headers: { Authorization: `Bearer ${effectiveKey}` },
|
|
77
|
+
cache: "no-store",
|
|
78
|
+
})
|
|
79
|
+
if (!res.ok) return
|
|
80
|
+
const json = (await res.json()) as {
|
|
81
|
+
data?: { variables: { key: string; value: string }[] }
|
|
82
|
+
}
|
|
83
|
+
const next: Record<string, string> = {}
|
|
84
|
+
for (const v of json.data?.variables ?? []) next[v.key] = v.value
|
|
85
|
+
setEnvs(next)
|
|
86
|
+
} catch {
|
|
87
|
+
// network error — keep previous envs, fail-soft
|
|
88
|
+
} finally {
|
|
89
|
+
setLoading(false)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!refreshIntervalMs || refreshIntervalMs <= 0) return
|
|
95
|
+
const id = setInterval(refresh, refreshIntervalMs)
|
|
96
|
+
return () => clearInterval(id)
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
|
+
}, [refreshIntervalMs, effectiveKey])
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<EnvContext.Provider value={{ envs, loading, refresh }}>
|
|
102
|
+
{children}
|
|
103
|
+
</EnvContext.Provider>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* `useEnv("KEY")` — provider'ın hydrate ettiği env değerini döner.
|
|
109
|
+
* Yoksa undefined; çağıran fallback verir (`useEnv("X") ?? "default"`).
|
|
110
|
+
*/
|
|
111
|
+
export function useEnv(key: string): string | undefined {
|
|
112
|
+
const ctx = useContext(EnvContext)
|
|
113
|
+
return ctx.envs[key]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Tüm public env'leri Record olarak döner. */
|
|
117
|
+
export function useAllEnvs(): Record<string, string> {
|
|
118
|
+
return useContext(EnvContext).envs
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Manuel refresh tetikleme (örn. admin "config updated" notification sonrası). */
|
|
122
|
+
export function useEnvRefresh(): { refresh: () => Promise<void>; loading: boolean } {
|
|
123
|
+
const ctx = useContext(EnvContext)
|
|
124
|
+
return { refresh: ctx.refresh, loading: ctx.loading }
|
|
125
|
+
}
|