@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.
@@ -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
+ }