@sentroy-co/client-sdk 2.6.4 → 2.8.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 +101 -2
- package/README.md +30 -1
- 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 +17 -3
- package/src/vault/index.ts +198 -0
- package/src/vault/react.tsx +125 -0
package/AGENTS.md
CHANGED
|
@@ -675,11 +675,110 @@ import {
|
|
|
675
675
|
} from "@sentroy-co/client-sdk/react"
|
|
676
676
|
```
|
|
677
677
|
|
|
678
|
+
## Env Vault (`@sentroy-co/client-sdk/vault` + `/vault/react`)
|
|
679
|
+
|
|
680
|
+
Sentroy Env Vault — centralized runtime env management. Bootstrap is a
|
|
681
|
+
single env (`SENTROY_ENV_API_KEY`); the rest of your config lives in
|
|
682
|
+
the dashboard at `vault.sentroy.com`. Changing a variable does NOT
|
|
683
|
+
require an app rebuild — the next read picks up the new value once the
|
|
684
|
+
in-memory cache TTL elapses (5 min default).
|
|
685
|
+
|
|
686
|
+
This module is intentionally **separate** from the main `Sentroy`
|
|
687
|
+
client. They use different auth namespaces:
|
|
688
|
+
|
|
689
|
+
| Surface | Token format | Scope |
|
|
690
|
+
|---|---|---|
|
|
691
|
+
| `Sentroy` (mail/storage) | `stk_...` | per-company access token |
|
|
692
|
+
| Env Vault | `stk_env_...` | per-(project, environment) bootstrap token |
|
|
693
|
+
|
|
694
|
+
### Server: `getEnv()`
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
import {
|
|
698
|
+
configureEnvClient, // optional one-shot config
|
|
699
|
+
getEnv, // async, undefined if missing
|
|
700
|
+
getEnvOrThrow, // async, throws if missing
|
|
701
|
+
getAllEnvs, // bulk: { KEY: value, ... }
|
|
702
|
+
getPublicEnvs, // bulk: only `public: true` variables
|
|
703
|
+
preloadEnv, // eager hydrate at process boot
|
|
704
|
+
refreshEnvCache, // manual invalidation hook
|
|
705
|
+
setEnvCacheTTL, // runtime TTL override (seconds)
|
|
706
|
+
} from "@sentroy-co/client-sdk/vault"
|
|
707
|
+
|
|
708
|
+
// Optional — defaults pull from process.env
|
|
709
|
+
configureEnvClient({
|
|
710
|
+
baseUrl: "https://sentroy.com", // or self-hosted Sentroy URL
|
|
711
|
+
apiKey: process.env.SENTROY_ENV_API_KEY, // default
|
|
712
|
+
ttlSeconds: 300, // 5 min
|
|
713
|
+
timeoutMs: 5000,
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
await preloadEnv() // optional: fail-fast if token invalid
|
|
717
|
+
|
|
718
|
+
const dbUrl = await getEnv("DATABASE_URL") // string | undefined
|
|
719
|
+
const turn = await getEnvOrThrow("TURNSTILE_SECRET") // string
|
|
720
|
+
const all = await getAllEnvs() // includes private
|
|
721
|
+
const pub = await getPublicEnvs() // public:true only
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
### React: SSR-injected provider + hook
|
|
725
|
+
|
|
726
|
+
```tsx
|
|
727
|
+
// app/layout.tsx (server component)
|
|
728
|
+
import { getPublicEnvs } from "@sentroy-co/client-sdk/vault"
|
|
729
|
+
import { EnvProvider } from "@sentroy-co/client-sdk/vault/react"
|
|
730
|
+
|
|
731
|
+
export default async function RootLayout({ children }) {
|
|
732
|
+
const envs = await getPublicEnvs() // public:true only — never leak secrets
|
|
733
|
+
return (
|
|
734
|
+
<html>
|
|
735
|
+
<body>
|
|
736
|
+
<EnvProvider envs={envs}>{children}</EnvProvider>
|
|
737
|
+
</body>
|
|
738
|
+
</html>
|
|
739
|
+
)
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
```tsx
|
|
744
|
+
// any "use client" component
|
|
745
|
+
"use client"
|
|
746
|
+
import { useEnv, useAllEnvs, useEnvRefresh } from "@sentroy-co/client-sdk/vault/react"
|
|
747
|
+
|
|
748
|
+
function CaptchaWidget() {
|
|
749
|
+
const siteKey = useEnv("TURNSTILE_SITE_KEY") // string | undefined
|
|
750
|
+
if (!siteKey) return null
|
|
751
|
+
return <Turnstile siteKey={siteKey} />
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function ConfigPanel() {
|
|
755
|
+
const all = useAllEnvs() // Record<string, string> — public envs only
|
|
756
|
+
const { refresh, loading } = useEnvRefresh()
|
|
757
|
+
return <button onClick={refresh} disabled={loading}>Refresh config</button>
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### `EnvProvider` props
|
|
762
|
+
|
|
763
|
+
| Prop | Type | Default | Notes |
|
|
764
|
+
|---|---|---|---|
|
|
765
|
+
| `envs` | `Record<string, string>` | required | SSR-fetched public envs |
|
|
766
|
+
| `refreshUrl` | `string` | `/api/env-vault/public` | Endpoint for client polling |
|
|
767
|
+
| `apiKey` | `string` | `process.env.NEXT_PUBLIC_SENTROY_ENV_API_KEY` | Bearer token for browser polling |
|
|
768
|
+
| `refreshIntervalMs` | `number` | `300000` (5 min) | `0` to disable polling |
|
|
769
|
+
|
|
770
|
+
### Security notes
|
|
771
|
+
|
|
772
|
+
- `useEnv()` only ever returns variables marked `public: true` in the dashboard. Server-only secrets stay server-side.
|
|
773
|
+
- The provider's polling is best-effort; network failures keep the previous values (fail-soft).
|
|
774
|
+
- The bootstrap token is per-(project, environment). A `prod` token cannot read `staging` and vice versa.
|
|
775
|
+
- Variable values are AES-256-GCM encrypted at rest in the Sentroy vault DB. Decryption happens server-side just before the fetch endpoint streams the response.
|
|
776
|
+
|
|
678
777
|
## Requirements
|
|
679
778
|
|
|
680
779
|
- Node.js 18+ (uses native `fetch`)
|
|
681
|
-
- React 18+ (only if you import from `/react`)
|
|
682
|
-
- Tailwind CSS in the host app (only for React components)
|
|
780
|
+
- React 18+ (only if you import from `/react` or `/vault/react`)
|
|
781
|
+
- Tailwind CSS in the host app (only for React UI components like `<MediaManager />`)
|
|
683
782
|
|
|
684
783
|
## Raw URL — for LLM/agent context windows
|
|
685
784
|
|
package/README.md
CHANGED
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
|
|
34
34
|
- **Mail** — verified domains, mailboxes, multi-language templates, IMAP-backed inbox, transactional and bulk send, suppressions, webhooks, audience lists, deliverability logs.
|
|
35
35
|
- **Storage** — isolated buckets, multipart uploads, image and media transformations, signed download URLs.
|
|
36
|
-
- **
|
|
36
|
+
- **Env Vault** — runtime env variable management (`@sentroy-co/client-sdk/vault`) — change a value in the dashboard, your app picks it up on the next read; no rebuild.
|
|
37
|
+
- **React drop-ins** — `<MediaManager />`, `<MediaManagerTrigger />` and `<EnvProvider>` / `useEnv()` (`@sentroy-co/client-sdk/react` + `@sentroy-co/client-sdk/vault/react`).
|
|
37
38
|
- **One client, two backends** — point at `https://sentroy.com` for the hosted platform, or your own deployment for self-hosted. Same API, same types.
|
|
38
39
|
|
|
39
40
|
## Install
|
|
@@ -77,6 +78,34 @@ console.log(media.url) // signed URL, served from the CDN
|
|
|
77
78
|
|
|
78
79
|
That's the smallest useful surface. Every other resource (`domains`, `mailboxes`, `templates`, `inbox`, `audience`, `webhooks`, `suppressions`, `logs`, `buckets`, `media`) follows the same `sentroy.<resource>.<verb>(...)` shape with full TypeScript types.
|
|
79
80
|
|
|
81
|
+
## Env Vault
|
|
82
|
+
|
|
83
|
+
Manage your env vars in the dashboard at [vault.sentroy.com](https://vault.sentroy.com), bootstrap your deploy with one token, and read values via a typed helper — no rebuild on change.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// server side
|
|
87
|
+
import { getEnv, getEnvOrThrow, preloadEnv } from "@sentroy-co/client-sdk/vault"
|
|
88
|
+
|
|
89
|
+
await preloadEnv() // optional fail-fast at boot
|
|
90
|
+
const dbUrl = await getEnv("DATABASE_URL")
|
|
91
|
+
const turnstile = await getEnvOrThrow("BETTER_AUTH_TURNSTILE_SECRET")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// React: SSR-injected provider + hook (no FOUC)
|
|
96
|
+
import { getPublicEnvs } from "@sentroy-co/client-sdk/vault"
|
|
97
|
+
import { EnvProvider, useEnv } from "@sentroy-co/client-sdk/vault/react"
|
|
98
|
+
|
|
99
|
+
// app/layout.tsx (server)
|
|
100
|
+
const envs = await getPublicEnvs()
|
|
101
|
+
return <EnvProvider envs={envs}>{children}</EnvProvider>
|
|
102
|
+
|
|
103
|
+
// any "use client" component
|
|
104
|
+
const siteKey = useEnv("TURNSTILE_SITE_KEY")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Bootstrap is a single env: `SENTROY_ENV_API_KEY`. Public/private split is enforced server-side — the React hook only ever sees `public: true` variables. Full reference at [docs.sentroy.com/env-vault](https://docs.sentroy.com/env-vault).
|
|
108
|
+
|
|
80
109
|
## Self-hosted vs hosted
|
|
81
110
|
|
|
82
111
|
The SDK is identical in both modes. Only `baseUrl` changes:
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
export interface EnvVariable {
|
|
28
|
+
key: string;
|
|
29
|
+
value: string;
|
|
30
|
+
type: string;
|
|
31
|
+
public: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface EnvCacheState {
|
|
34
|
+
fetchedAt: number;
|
|
35
|
+
variables: Map<string, EnvVariable>;
|
|
36
|
+
project: string;
|
|
37
|
+
environment: string;
|
|
38
|
+
}
|
|
39
|
+
interface ClientOptions {
|
|
40
|
+
/** Sentroy core URL (defaults to env or https://sentroy.com). */
|
|
41
|
+
baseUrl?: string;
|
|
42
|
+
/** API key — defaults to `process.env.SENTROY_ENV_API_KEY`. */
|
|
43
|
+
apiKey?: string;
|
|
44
|
+
/** Cache TTL in seconds; default 300. */
|
|
45
|
+
ttlSeconds?: number;
|
|
46
|
+
/** Fetch timeout in ms; default 5000. */
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* One-time client config — Sentroy app'lerinde modül seviyesinde çağrılır,
|
|
51
|
+
* default'lara güvenilirse hiç çağrılmasına gerek yok.
|
|
52
|
+
*/
|
|
53
|
+
export declare function configureEnvClient(options?: ClientOptions): void;
|
|
54
|
+
/** TTL'i runtime'da değiştir (örn. development için kısa, prod için uzun). */
|
|
55
|
+
export declare function setEnvCacheTTL(seconds: number): void;
|
|
56
|
+
/** Cache'i invalidate et — webhook ya da admin-driven manual refresh için. */
|
|
57
|
+
export declare function refreshEnvCache(): Promise<void>;
|
|
58
|
+
/** Process start'ında erkenden tetikle — eksik env'i fail-fast yakalar. */
|
|
59
|
+
export declare function preloadEnv(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Async — env yoksa undefined. Bu fonksiyon TÜM env'leri (server+public)
|
|
62
|
+
* gizler, çünkü `process.env` fallback yok; sadece vault'ta kayıtlı
|
|
63
|
+
* olanlar dönder. Token bootstrap fail ederse exception atar.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getEnv(key: string): Promise<string | undefined>;
|
|
66
|
+
/** Eksik env'i hemen patlatır — config-validation pattern'inde kullanışlı. */
|
|
67
|
+
export declare function getEnvOrThrow(key: string): Promise<string>;
|
|
68
|
+
/** Tüm env'leri map olarak döner (dump için kullanışlı). */
|
|
69
|
+
export declare function getAllEnvs(): Promise<Record<string, string>>;
|
|
70
|
+
/** Sadece public (`public: true`) env'ler — SSR helper için. */
|
|
71
|
+
export declare function getPublicEnvs(): Promise<Record<string, string>>;
|
|
72
|
+
export {};
|
|
73
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vault/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;CACpB;AAKD,UAAU,aAAa;IACrB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAcD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,aAAkB,GAAG,IAAI,CAYpE;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEpD;AAED,8EAA8E;AAC9E,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAGrD;AAED,2EAA2E;AAC3E,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAEhD;AA6DD;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAGrE;AAED,8EAA8E;AAC9E,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQhE;AAED,4DAA4D;AAC5D,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAKlE;AAED,gEAAgE;AAChE,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAOrE"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `@sentroy-co/client-sdk/vault` — Sentroy Env Vault server-side client.
|
|
4
|
+
*
|
|
5
|
+
* Bootstrap pattern:
|
|
6
|
+
* `SENTROY_ENV_API_KEY` (process.env) → tek dış env. Fonksiyonlar
|
|
7
|
+
* `getEnv("KEY")` ya da `getEnvOrThrow("KEY")` çağrılırken in-memory
|
|
8
|
+
* cache'den döner; ilk çağrıda Sentroy core'a HTTP fetch yapar ve
|
|
9
|
+
* o token scope'undaki TÜM env'leri (public + private) çeker.
|
|
10
|
+
*
|
|
11
|
+
* Cache stratejisi:
|
|
12
|
+
* • Default TTL 5 dk; refresh deadline aşıldığında bir sonraki
|
|
13
|
+
* `getEnv` çağrısında re-fetch tetiklenir.
|
|
14
|
+
* • `await refreshEnvCache()` manuel invalidation — webhook ya da
|
|
15
|
+
* SIGHUP-style restart sinyaline bağlanabilir.
|
|
16
|
+
* • `setEnvCacheTTL(seconds)` runtime'da TTL değiştirme.
|
|
17
|
+
*
|
|
18
|
+
* Hata politikası: bootstrap fail (token yok / network down / 401)
|
|
19
|
+
* → `getEnv` her çağrıda undefined döner; `getEnvOrThrow` exception
|
|
20
|
+
* atar. Process startup'ında `await preloadEnv()` çağırırsanız
|
|
21
|
+
* eksik env'leri erkenden yakalarsınız.
|
|
22
|
+
*
|
|
23
|
+
* **NOT**: Bu modül `Sentroy` ana client'ından (mail/storage REST
|
|
24
|
+
* resource'ları) bağımsızdır. Vault token'ları (stk_env_*) ile mail/
|
|
25
|
+
* storage token'ları (stk_*) farklı namespace'tedir; tek client'ta
|
|
26
|
+
* birleştirmek ergonomiyi bozardı.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.configureEnvClient = configureEnvClient;
|
|
30
|
+
exports.setEnvCacheTTL = setEnvCacheTTL;
|
|
31
|
+
exports.refreshEnvCache = refreshEnvCache;
|
|
32
|
+
exports.preloadEnv = preloadEnv;
|
|
33
|
+
exports.getEnv = getEnv;
|
|
34
|
+
exports.getEnvOrThrow = getEnvOrThrow;
|
|
35
|
+
exports.getAllEnvs = getAllEnvs;
|
|
36
|
+
exports.getPublicEnvs = getPublicEnvs;
|
|
37
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
38
|
+
const DEFAULT_BASE_URL = "https://sentroy.com";
|
|
39
|
+
let resolvedBaseUrl = DEFAULT_BASE_URL;
|
|
40
|
+
let resolvedApiKey;
|
|
41
|
+
let cacheTtlMs = DEFAULT_TTL_MS;
|
|
42
|
+
let fetchTimeoutMs = 5000;
|
|
43
|
+
let cache = null;
|
|
44
|
+
let pendingRefresh = null;
|
|
45
|
+
function readEnv(name) {
|
|
46
|
+
if (typeof process === "undefined")
|
|
47
|
+
return undefined;
|
|
48
|
+
return process.env?.[name];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One-time client config — Sentroy app'lerinde modül seviyesinde çağrılır,
|
|
52
|
+
* default'lara güvenilirse hiç çağrılmasına gerek yok.
|
|
53
|
+
*/
|
|
54
|
+
function configureEnvClient(options = {}) {
|
|
55
|
+
if (options.baseUrl)
|
|
56
|
+
resolvedBaseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
57
|
+
else
|
|
58
|
+
resolvedBaseUrl = (readEnv("NEXT_PUBLIC_SENTROY_ENV_API_URL") ||
|
|
59
|
+
readEnv("SENTROY_ENV_API_URL") ||
|
|
60
|
+
readEnv("NEXT_PUBLIC_CORE_APP_URL") ||
|
|
61
|
+
DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
62
|
+
resolvedApiKey = options.apiKey ?? readEnv("SENTROY_ENV_API_KEY");
|
|
63
|
+
if (options.ttlSeconds)
|
|
64
|
+
cacheTtlMs = options.ttlSeconds * 1000;
|
|
65
|
+
if (options.timeoutMs)
|
|
66
|
+
fetchTimeoutMs = options.timeoutMs;
|
|
67
|
+
}
|
|
68
|
+
/** TTL'i runtime'da değiştir (örn. development için kısa, prod için uzun). */
|
|
69
|
+
function setEnvCacheTTL(seconds) {
|
|
70
|
+
cacheTtlMs = seconds * 1000;
|
|
71
|
+
}
|
|
72
|
+
/** Cache'i invalidate et — webhook ya da admin-driven manual refresh için. */
|
|
73
|
+
async function refreshEnvCache() {
|
|
74
|
+
cache = null;
|
|
75
|
+
await ensureCache();
|
|
76
|
+
}
|
|
77
|
+
/** Process start'ında erkenden tetikle — eksik env'i fail-fast yakalar. */
|
|
78
|
+
async function preloadEnv() {
|
|
79
|
+
await ensureCache();
|
|
80
|
+
}
|
|
81
|
+
async function fetchVariables() {
|
|
82
|
+
if (!resolvedApiKey) {
|
|
83
|
+
// Lazy bootstrap — configureEnvClient çağrılmadıysa env'den oku.
|
|
84
|
+
configureEnvClient();
|
|
85
|
+
}
|
|
86
|
+
if (!resolvedApiKey) {
|
|
87
|
+
throw new Error("@sentroy-co/client-sdk/vault: SENTROY_ENV_API_KEY is not set. " +
|
|
88
|
+
"Set it on the platform (Coolify env) or call configureEnvClient({ apiKey: ... }) at boot.");
|
|
89
|
+
}
|
|
90
|
+
const url = `${resolvedBaseUrl}/api/env-vault/fetch`;
|
|
91
|
+
const res = await fetch(url, {
|
|
92
|
+
headers: { Authorization: `Bearer ${resolvedApiKey}` },
|
|
93
|
+
signal: AbortSignal.timeout(fetchTimeoutMs),
|
|
94
|
+
cache: "no-store",
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
throw new Error(`env-vault fetch failed: ${res.status} ${res.statusText} (url=${url})`);
|
|
98
|
+
}
|
|
99
|
+
const json = (await res.json());
|
|
100
|
+
if (!json.data)
|
|
101
|
+
throw new Error("env-vault fetch: malformed response");
|
|
102
|
+
const map = new Map();
|
|
103
|
+
for (const v of json.data.variables)
|
|
104
|
+
map.set(v.key, v);
|
|
105
|
+
return {
|
|
106
|
+
fetchedAt: Date.now(),
|
|
107
|
+
variables: map,
|
|
108
|
+
project: json.data.project,
|
|
109
|
+
environment: json.data.environment,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function ensureCache() {
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
if (cache && now - cache.fetchedAt < cacheTtlMs)
|
|
115
|
+
return cache;
|
|
116
|
+
if (pendingRefresh) {
|
|
117
|
+
await pendingRefresh;
|
|
118
|
+
if (cache)
|
|
119
|
+
return cache;
|
|
120
|
+
}
|
|
121
|
+
pendingRefresh = (async () => {
|
|
122
|
+
try {
|
|
123
|
+
cache = await fetchVariables();
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
pendingRefresh = null;
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
await pendingRefresh;
|
|
130
|
+
if (!cache)
|
|
131
|
+
throw new Error("env-vault: cache hydrate failed");
|
|
132
|
+
return cache;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Async — env yoksa undefined. Bu fonksiyon TÜM env'leri (server+public)
|
|
136
|
+
* gizler, çünkü `process.env` fallback yok; sadece vault'ta kayıtlı
|
|
137
|
+
* olanlar dönder. Token bootstrap fail ederse exception atar.
|
|
138
|
+
*/
|
|
139
|
+
async function getEnv(key) {
|
|
140
|
+
const c = await ensureCache();
|
|
141
|
+
return c.variables.get(key)?.value;
|
|
142
|
+
}
|
|
143
|
+
/** Eksik env'i hemen patlatır — config-validation pattern'inde kullanışlı. */
|
|
144
|
+
async function getEnvOrThrow(key) {
|
|
145
|
+
const v = await getEnv(key);
|
|
146
|
+
if (v === undefined) {
|
|
147
|
+
throw new Error(`env-vault: required variable ${key} is not defined (project=${cache?.project ?? "?"}, env=${cache?.environment ?? "?"})`);
|
|
148
|
+
}
|
|
149
|
+
return v;
|
|
150
|
+
}
|
|
151
|
+
/** Tüm env'leri map olarak döner (dump için kullanışlı). */
|
|
152
|
+
async function getAllEnvs() {
|
|
153
|
+
const c = await ensureCache();
|
|
154
|
+
const out = {};
|
|
155
|
+
for (const [k, v] of c.variables)
|
|
156
|
+
out[k] = v.value;
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
/** Sadece public (`public: true`) env'ler — SSR helper için. */
|
|
160
|
+
async function getPublicEnvs() {
|
|
161
|
+
const c = await ensureCache();
|
|
162
|
+
const out = {};
|
|
163
|
+
for (const [k, v] of c.variables) {
|
|
164
|
+
if (v.public)
|
|
165
|
+
out[k] = v.value;
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vault/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
interface EnvProviderProps {
|
|
3
|
+
/** Server-side fetched public envs — SSR'da inject edilir. */
|
|
4
|
+
envs: Record<string, string>;
|
|
5
|
+
/** Public refresh endpoint URL — default `/api/env-vault/public`. */
|
|
6
|
+
refreshUrl?: string;
|
|
7
|
+
/** Bearer token — public endpoint için. Default `process.env.NEXT_PUBLIC_SENTROY_ENV_API_KEY`. */
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
/** Refresh interval ms; 0 ise polling kapalı. Default 5 dk. */
|
|
10
|
+
refreshIntervalMs?: number;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare function EnvProvider({ envs: initialEnvs, refreshUrl, apiKey, refreshIntervalMs, children, }: EnvProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
/**
|
|
15
|
+
* `useEnv("KEY")` — provider'ın hydrate ettiği env değerini döner.
|
|
16
|
+
* Yoksa undefined; çağıran fallback verir (`useEnv("X") ?? "default"`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function useEnv(key: string): string | undefined;
|
|
19
|
+
/** Tüm public env'leri Record olarak döner. */
|
|
20
|
+
export declare function useAllEnvs(): Record<string, string>;
|
|
21
|
+
/** Manuel refresh tetikleme (örn. admin "config updated" notification sonrası). */
|
|
22
|
+
export declare function useEnvRefresh(): {
|
|
23
|
+
refresh: () => Promise<void>;
|
|
24
|
+
loading: boolean;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/vault/react.tsx"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AAgCd,UAAU,gBAAgB;IACxB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kGAAkG;IAClG,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,EAAE,SAAS,CAAA;CACpB;AAID,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EAAE,WAAW,EACjB,UAAoC,EACpC,MAAM,EACN,iBAA+C,EAC/C,QAAQ,GACT,EAAE,gBAAgB,2CA4ClB;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGtD;AAED,+CAA+C;AAC/C,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEnD;AAED,mFAAmF;AACnF,wBAAgB,aAAa,IAAI;IAAE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAGlF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.EnvProvider = EnvProvider;
|
|
5
|
+
exports.useEnv = useEnv;
|
|
6
|
+
exports.useAllEnvs = useAllEnvs;
|
|
7
|
+
exports.useEnvRefresh = useEnvRefresh;
|
|
8
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
9
|
+
const react_1 = require("react");
|
|
10
|
+
const EnvContext = (0, react_1.createContext)({
|
|
11
|
+
envs: {},
|
|
12
|
+
loading: false,
|
|
13
|
+
refresh: async () => { },
|
|
14
|
+
});
|
|
15
|
+
const DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
16
|
+
function EnvProvider({ envs: initialEnvs, refreshUrl = "/api/env-vault/public", apiKey, refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS, children, }) {
|
|
17
|
+
const [envs, setEnvs] = (0, react_1.useState)(initialEnvs);
|
|
18
|
+
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
19
|
+
const effectiveKey = apiKey ??
|
|
20
|
+
(typeof process !== "undefined"
|
|
21
|
+
? process.env?.NEXT_PUBLIC_SENTROY_ENV_API_KEY
|
|
22
|
+
: undefined);
|
|
23
|
+
async function refresh() {
|
|
24
|
+
if (!effectiveKey)
|
|
25
|
+
return; // bootstrap yoksa polling no-op
|
|
26
|
+
setLoading(true);
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(refreshUrl, {
|
|
29
|
+
headers: { Authorization: `Bearer ${effectiveKey}` },
|
|
30
|
+
cache: "no-store",
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok)
|
|
33
|
+
return;
|
|
34
|
+
const json = (await res.json());
|
|
35
|
+
const next = {};
|
|
36
|
+
for (const v of json.data?.variables ?? [])
|
|
37
|
+
next[v.key] = v.value;
|
|
38
|
+
setEnvs(next);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// network error — keep previous envs, fail-soft
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
(0, react_1.useEffect)(() => {
|
|
48
|
+
if (!refreshIntervalMs || refreshIntervalMs <= 0)
|
|
49
|
+
return;
|
|
50
|
+
const id = setInterval(refresh, refreshIntervalMs);
|
|
51
|
+
return () => clearInterval(id);
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
}, [refreshIntervalMs, effectiveKey]);
|
|
54
|
+
return ((0, jsx_runtime_1.jsx)(EnvContext.Provider, { value: { envs, loading, refresh }, children: children }));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `useEnv("KEY")` — provider'ın hydrate ettiği env değerini döner.
|
|
58
|
+
* Yoksa undefined; çağıran fallback verir (`useEnv("X") ?? "default"`).
|
|
59
|
+
*/
|
|
60
|
+
function useEnv(key) {
|
|
61
|
+
const ctx = (0, react_1.useContext)(EnvContext);
|
|
62
|
+
return ctx.envs[key];
|
|
63
|
+
}
|
|
64
|
+
/** Tüm public env'leri Record olarak döner. */
|
|
65
|
+
function useAllEnvs() {
|
|
66
|
+
return (0, react_1.useContext)(EnvContext).envs;
|
|
67
|
+
}
|
|
68
|
+
/** Manuel refresh tetikleme (örn. admin "config updated" notification sonrası). */
|
|
69
|
+
function useEnvRefresh() {
|
|
70
|
+
const ctx = (0, react_1.useContext)(EnvContext);
|
|
71
|
+
return { refresh: ctx.refresh, loading: ctx.loading };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/vault/react.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentroy-co/client-sdk",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "TypeScript SDK for the Sentroy platform —
|
|
3
|
+
"version": "2.8.0",
|
|
4
|
+
"description": "TypeScript SDK for the Sentroy platform — mail, storage, env vault + React components.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
"types": "./dist/react/crop/index.d.ts",
|
|
25
25
|
"import": "./dist/react/crop/index.js",
|
|
26
26
|
"require": "./dist/react/crop/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./vault": {
|
|
29
|
+
"types": "./dist/vault/index.d.ts",
|
|
30
|
+
"import": "./dist/vault/index.js",
|
|
31
|
+
"require": "./dist/vault/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./vault/react": {
|
|
34
|
+
"types": "./dist/vault/react.d.ts",
|
|
35
|
+
"import": "./dist/vault/react.js",
|
|
36
|
+
"require": "./dist/vault/react.js"
|
|
27
37
|
}
|
|
28
38
|
},
|
|
29
39
|
"files": [
|
|
@@ -43,7 +53,11 @@
|
|
|
43
53
|
"mail",
|
|
44
54
|
"typescript",
|
|
45
55
|
"react",
|
|
46
|
-
"media-manager"
|
|
56
|
+
"media-manager",
|
|
57
|
+
"env",
|
|
58
|
+
"vault",
|
|
59
|
+
"secrets",
|
|
60
|
+
"config"
|
|
47
61
|
],
|
|
48
62
|
"repository": {
|
|
49
63
|
"type": "git",
|
|
@@ -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
|
+
}
|