@sentroy-co/client-sdk 2.9.0 → 2.13.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 +74 -0
- package/README.md +20 -1
- package/dist/vault/index.d.ts +60 -0
- package/dist/vault/index.d.ts.map +1 -1
- package/dist/vault/index.js +144 -0
- package/dist/vault/index.js.map +1 -1
- package/package.json +1 -1
- package/src/vault/index.ts +196 -0
package/AGENTS.md
CHANGED
|
@@ -767,6 +767,35 @@ function ConfigPanel() {
|
|
|
767
767
|
| `apiKey` | `string` | `process.env.NEXT_PUBLIC_SENTROY_ENV_API_KEY` | Bearer token for browser polling |
|
|
768
768
|
| `refreshIntervalMs` | `number` | `300000` (5 min) | `0` to disable polling |
|
|
769
769
|
|
|
770
|
+
### Debug logging (`SENTROY_ENV_DEBUG`)
|
|
771
|
+
|
|
772
|
+
Set `SENTROY_ENV_DEBUG=1` (or `true`) on the consuming app to surface every fetch, cache hit, and fallback through `console.log`. Sample output:
|
|
773
|
+
|
|
774
|
+
```
|
|
775
|
+
[env-vault] fetching https://sentroy.com/api/env-vault/fetch
|
|
776
|
+
[env-vault] fetched 7 var(s) from sentroy-core/prod in 234ms
|
|
777
|
+
[env-vault] BETTER_AUTH_TURNSTILE_SECRET: vault hit
|
|
778
|
+
[env-vault] IPINFO_TOKEN: vault miss → process.env fallback
|
|
779
|
+
[env-vault] AI_GATEWAY_API_KEY: vault error (...) → process.env fallback
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Default is off — turn on temporarily after a deploy to verify migrations are reading from the right source, then turn off to keep prod logs clean.
|
|
783
|
+
|
|
784
|
+
### Migration helper: `getEnvWithFallback(key)`
|
|
785
|
+
|
|
786
|
+
For codebases moving from `process.env` to vault gradually, use `getEnvWithFallback` — it tries vault first, falls back to `process.env[key]` on cache miss / fetch failure / missing token. The point is *zero downtime*: deploy the code change before populating the vault, and nothing breaks; fill the vault later, and the same code starts reading from there.
|
|
787
|
+
|
|
788
|
+
```ts
|
|
789
|
+
import { getEnvWithFallback } from "@sentroy-co/client-sdk/vault"
|
|
790
|
+
|
|
791
|
+
// Old: process.env.STRIPE_SECRET_KEY
|
|
792
|
+
const stripeKey = await getEnvWithFallback("STRIPE_SECRET_KEY")
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
After the value is in the vault and you've verified it's being read, swap the call to `getEnv` (or `getEnvOrThrow`) so a future `process.env` re-introduction doesn't silently shadow the vault value.
|
|
796
|
+
|
|
797
|
+
Bootstrap path (no `SENTROY_ENV_API_KEY` set) skips the fetch entirely and goes straight to `process.env` — so an app deployed without vault credentials still boots and reads its envs the legacy way. This is intentional: the vault is opt-in, not a hard requirement.
|
|
798
|
+
|
|
770
799
|
### Security notes
|
|
771
800
|
|
|
772
801
|
- `useEnv()` only ever returns variables marked `public: true` in the dashboard. Server-only secrets stay server-side.
|
|
@@ -774,6 +803,51 @@ function ConfigPanel() {
|
|
|
774
803
|
- The bootstrap token is per-(project, environment). A `prod` token cannot read `staging` and vice versa.
|
|
775
804
|
- 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
805
|
|
|
806
|
+
### Webhooks (`createVaultWebhookHandler`)
|
|
807
|
+
|
|
808
|
+
Variable changes can push directly to your app instead of waiting on the 5-min cache TTL. Configure a webhook in the dashboard under a project's **Webhooks** tab — Sentroy will POST to your URL on every `variable.create | variable.update | variable.delete`.
|
|
809
|
+
|
|
810
|
+
```ts
|
|
811
|
+
// app/api/sentroy/vault-webhook/route.ts
|
|
812
|
+
import { createVaultWebhookHandler } from "@sentroy-co/client-sdk/vault"
|
|
813
|
+
|
|
814
|
+
export const POST = createVaultWebhookHandler({
|
|
815
|
+
secret: process.env.SENTROY_VAULT_WEBHOOK_SECRET!,
|
|
816
|
+
// optional — default behaviour: await refreshEnvCache()
|
|
817
|
+
async onChange(payload) {
|
|
818
|
+
console.log("vault changed", payload.action, payload.keys)
|
|
819
|
+
// your invalidation logic, then:
|
|
820
|
+
await refreshEnvCache()
|
|
821
|
+
},
|
|
822
|
+
// optional — replay-window check, default 5 min
|
|
823
|
+
maxAgeMs: 5 * 60 * 1000,
|
|
824
|
+
})
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
Payload (signed):
|
|
828
|
+
```json
|
|
829
|
+
{
|
|
830
|
+
"event": "vault.variable.changed",
|
|
831
|
+
"project": "<projectId>",
|
|
832
|
+
"environment": "prod",
|
|
833
|
+
"action": "create" | "update" | "delete",
|
|
834
|
+
"keys": ["DATABASE_URL", "..."],
|
|
835
|
+
"timestamp": 1731430000000
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
Headers Sentroy sends: `X-Sentroy-Signature: sha256=<hex>` (HMAC over the raw body), `X-Sentroy-Event: vault.variable.changed`, `X-Sentroy-Webhook-Id: <id>`.
|
|
840
|
+
|
|
841
|
+
The handler returns:
|
|
842
|
+
- `200` with `{ ok: true }` after a verified signature + completed `onChange`
|
|
843
|
+
- `401` for missing/malformed/invalid signature, or timestamp outside the replay window
|
|
844
|
+
- `400` for an invalid JSON body
|
|
845
|
+
- `500` if `onChange` throws
|
|
846
|
+
|
|
847
|
+
Delivery is fire-and-forget on the Sentroy side with a 5 sec timeout; the dashboard records the last delivery's status + error string per webhook for visibility. Failed deliveries are not auto-retried (admin can flip the enabled toggle to retry manually by re-saving a variable, or we'll add a "resend" button later).
|
|
848
|
+
|
|
849
|
+
The vault webhook secret namespace is `whsec_*` — distinct from access tokens (`stk_*` / `stk_env_*`).
|
|
850
|
+
|
|
777
851
|
### CLI (`sentroy env ...`)
|
|
778
852
|
|
|
779
853
|
The package ships a `sentroy` binary. After `npm install` (or `npm install -g`) it's available on `PATH`; `npx sentroy ...` works without a global install.
|
package/README.md
CHANGED
|
@@ -84,11 +84,15 @@ Manage your env vars in the dashboard at [vault.sentroy.com](https://vault.sentr
|
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
86
|
// server side
|
|
87
|
-
import { getEnv, getEnvOrThrow, preloadEnv } from "@sentroy-co/client-sdk/vault"
|
|
87
|
+
import { getEnv, getEnvOrThrow, getEnvWithFallback, preloadEnv } from "@sentroy-co/client-sdk/vault"
|
|
88
88
|
|
|
89
89
|
await preloadEnv() // optional fail-fast at boot
|
|
90
90
|
const dbUrl = await getEnv("DATABASE_URL")
|
|
91
91
|
const turnstile = await getEnvOrThrow("BETTER_AUTH_TURNSTILE_SECRET")
|
|
92
|
+
|
|
93
|
+
// Migration helper — vault'tan oku, yoksa process.env fallback.
|
|
94
|
+
// Sentroy app'lerini kademeli olarak migrate ederken kullanışlı.
|
|
95
|
+
const stripe = await getEnvWithFallback("STRIPE_SECRET_KEY")
|
|
92
96
|
```
|
|
93
97
|
|
|
94
98
|
```tsx
|
|
@@ -106,6 +110,21 @@ const siteKey = useEnv("TURNSTILE_SITE_KEY")
|
|
|
106
110
|
|
|
107
111
|
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
112
|
|
|
113
|
+
### Webhooks (real-time invalidation)
|
|
114
|
+
|
|
115
|
+
Skip the 5-min cache TTL — point the vault at your app and it'll POST whenever any variable changes. The default handler verifies the HMAC-SHA256 signature and refreshes the cache:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// app/api/sentroy/vault-webhook/route.ts
|
|
119
|
+
import { createVaultWebhookHandler } from "@sentroy-co/client-sdk/vault"
|
|
120
|
+
|
|
121
|
+
export const POST = createVaultWebhookHandler({
|
|
122
|
+
secret: process.env.SENTROY_VAULT_WEBHOOK_SECRET!,
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Configure the receiver URL in the vault dashboard under the project's **Webhooks** tab; the secret comes back once at create-time. Provide your own `onChange` handler for custom logic.
|
|
127
|
+
|
|
109
128
|
### CLI
|
|
110
129
|
|
|
111
130
|
The package ships a `sentroy` CLI for syncing local `.env` files to the vault — useful for build pipelines and onboarding.
|
package/dist/vault/index.d.ts
CHANGED
|
@@ -65,9 +65,69 @@ export declare function preloadEnv(): Promise<void>;
|
|
|
65
65
|
export declare function getEnv(key: string): Promise<string | undefined>;
|
|
66
66
|
/** Eksik env'i hemen patlatır — config-validation pattern'inde kullanışlı. */
|
|
67
67
|
export declare function getEnvOrThrow(key: string): Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* Migration helper — vault'tan oku, yoksa `process.env` fallback.
|
|
70
|
+
*
|
|
71
|
+
* Sentroy app'lerini kademeli olarak `process.env` → vault'a çevirirken
|
|
72
|
+
* "her ikisi de çalışsın" senaryosu için. Vault doldurulmamış / token
|
|
73
|
+
* eksik / fetch fail dönerse sessizce `process.env[key]`'e döner — eski
|
|
74
|
+
* deploy ile yeni kod bir arada çalışabilir.
|
|
75
|
+
*
|
|
76
|
+
* **Migration tamamlandıktan sonra** çağrı sitelerini `getEnv()` ya da
|
|
77
|
+
* `getEnvOrThrow()`'a çevir; fallback'i bırakmak silently process.env
|
|
78
|
+
* sızıntısı riskini taşır (kullanıcı vault'tan key'i sildi sansa bile
|
|
79
|
+
* eski process.env değeri etkili olur).
|
|
80
|
+
*
|
|
81
|
+
* Bootstrap path için (`SENTROY_ENV_API_KEY` set değil) doğrudan
|
|
82
|
+
* `process.env`'e döner — vault fetch denemez. Bu önemli: Sentroy app'i
|
|
83
|
+
* vault'sız boot edilebilir.
|
|
84
|
+
*/
|
|
85
|
+
export declare function getEnvWithFallback(key: string): Promise<string | undefined>;
|
|
68
86
|
/** Tüm env'leri map olarak döner (dump için kullanışlı). */
|
|
69
87
|
export declare function getAllEnvs(): Promise<Record<string, string>>;
|
|
70
88
|
/** Sadece public (`public: true`) env'ler — SSR helper için. */
|
|
71
89
|
export declare function getPublicEnvs(): Promise<Record<string, string>>;
|
|
90
|
+
export interface VaultWebhookPayload {
|
|
91
|
+
event: "vault.variable.changed";
|
|
92
|
+
project: string;
|
|
93
|
+
environment: string;
|
|
94
|
+
action: "create" | "update" | "delete";
|
|
95
|
+
/** Etkilenen key'ler — bulk push'ta birden fazla. */
|
|
96
|
+
keys: string[];
|
|
97
|
+
/** Unix ms. */
|
|
98
|
+
timestamp: number;
|
|
99
|
+
}
|
|
100
|
+
export interface CreateVaultWebhookHandlerOptions {
|
|
101
|
+
/**
|
|
102
|
+
* Sentroy vault dashboard'dan aldığın webhook secret (`whsec_...`).
|
|
103
|
+
* Receiver bu secret'la HMAC-SHA256 imzayı doğrular; hatalıysa 401 döner.
|
|
104
|
+
*/
|
|
105
|
+
secret: string;
|
|
106
|
+
/**
|
|
107
|
+
* Imzayı doğruladıktan sonra çağrılır. Default davranış:
|
|
108
|
+
* `await refreshEnvCache()` — bir sonraki getEnv() taze değerleri çeker.
|
|
109
|
+
* Custom logic için override et (örn. tek bir key'i targeted invalidate).
|
|
110
|
+
*/
|
|
111
|
+
onChange?: (payload: VaultWebhookPayload) => Promise<void> | void;
|
|
112
|
+
/**
|
|
113
|
+
* Replay attack'lere karşı body'nin timestamp'i ile şu an arasındaki
|
|
114
|
+
* maksimum tolerans (ms). Default 5 dk. Sıfır ise check kapalı.
|
|
115
|
+
*/
|
|
116
|
+
maxAgeMs?: number;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Bir Sentroy vault webhook receiver'ı için Request → Response handler
|
|
120
|
+
* üretir. Next.js App Router'da:
|
|
121
|
+
*
|
|
122
|
+
* // app/api/sentroy/vault-webhook/route.ts
|
|
123
|
+
* import { createVaultWebhookHandler } from "@sentroy-co/client-sdk/vault"
|
|
124
|
+
* export const POST = createVaultWebhookHandler({
|
|
125
|
+
* secret: process.env.SENTROY_VAULT_WEBHOOK_SECRET!,
|
|
126
|
+
* })
|
|
127
|
+
*
|
|
128
|
+
* Default davranış: imza doğruysa cache'i invalidate eder ve 200 döner.
|
|
129
|
+
* Hatalı/eksik imza → 401, eski timestamp → 401, body parse hatası → 400.
|
|
130
|
+
*/
|
|
131
|
+
export declare function createVaultWebhookHandler(options: CreateVaultWebhookHandlerOptions): (request: Request) => Promise<Response>;
|
|
72
132
|
export {};
|
|
73
133
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +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;
|
|
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;AAsBD,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;AAmED;;;;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;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAwB7B;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;AAID,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,wBAAwB,CAAA;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;IACtC,qDAAqD;IACrD,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,eAAe;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gCAAgC;IAC/C;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACjE;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AA8BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gCAAgC,GACxC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAmDzC"}
|
package/dist/vault/index.js
CHANGED
|
@@ -32,10 +32,27 @@ exports.refreshEnvCache = refreshEnvCache;
|
|
|
32
32
|
exports.preloadEnv = preloadEnv;
|
|
33
33
|
exports.getEnv = getEnv;
|
|
34
34
|
exports.getEnvOrThrow = getEnvOrThrow;
|
|
35
|
+
exports.getEnvWithFallback = getEnvWithFallback;
|
|
35
36
|
exports.getAllEnvs = getAllEnvs;
|
|
36
37
|
exports.getPublicEnvs = getPublicEnvs;
|
|
38
|
+
exports.createVaultWebhookHandler = createVaultWebhookHandler;
|
|
37
39
|
const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
38
40
|
const DEFAULT_BASE_URL = "https://sentroy.com";
|
|
41
|
+
/**
|
|
42
|
+
* Opt-in runtime logging — `SENTROY_ENV_DEBUG=1` set edilirse her fetch
|
|
43
|
+
* (success/fail), cache hit ve fallback satırı stdout'a yazılır. Default
|
|
44
|
+
* kapalı; migration verification + prod sorunu debug için.
|
|
45
|
+
*/
|
|
46
|
+
function isDebug() {
|
|
47
|
+
const v = readEnv("SENTROY_ENV_DEBUG");
|
|
48
|
+
return v === "1" || v === "true";
|
|
49
|
+
}
|
|
50
|
+
function debugLog(...parts) {
|
|
51
|
+
if (isDebug()) {
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
console.log("[env-vault]", ...parts);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
39
56
|
let resolvedBaseUrl = DEFAULT_BASE_URL;
|
|
40
57
|
let resolvedApiKey;
|
|
41
58
|
let cacheTtlMs = DEFAULT_TTL_MS;
|
|
@@ -88,12 +105,15 @@ async function fetchVariables() {
|
|
|
88
105
|
"Set it on the platform (Coolify env) or call configureEnvClient({ apiKey: ... }) at boot.");
|
|
89
106
|
}
|
|
90
107
|
const url = `${resolvedBaseUrl}/api/env-vault/fetch`;
|
|
108
|
+
const started = Date.now();
|
|
109
|
+
debugLog(`fetching ${url}`);
|
|
91
110
|
const res = await fetch(url, {
|
|
92
111
|
headers: { Authorization: `Bearer ${resolvedApiKey}` },
|
|
93
112
|
signal: AbortSignal.timeout(fetchTimeoutMs),
|
|
94
113
|
cache: "no-store",
|
|
95
114
|
});
|
|
96
115
|
if (!res.ok) {
|
|
116
|
+
debugLog(`fetch failed: ${res.status} ${res.statusText} (${Date.now() - started}ms)`);
|
|
97
117
|
throw new Error(`env-vault fetch failed: ${res.status} ${res.statusText} (url=${url})`);
|
|
98
118
|
}
|
|
99
119
|
const json = (await res.json());
|
|
@@ -102,6 +122,7 @@ async function fetchVariables() {
|
|
|
102
122
|
const map = new Map();
|
|
103
123
|
for (const v of json.data.variables)
|
|
104
124
|
map.set(v.key, v);
|
|
125
|
+
debugLog(`fetched ${map.size} var(s) from ${json.data.project}/${json.data.environment} in ${Date.now() - started}ms`);
|
|
105
126
|
return {
|
|
106
127
|
fetchedAt: Date.now(),
|
|
107
128
|
variables: map,
|
|
@@ -148,6 +169,47 @@ async function getEnvOrThrow(key) {
|
|
|
148
169
|
}
|
|
149
170
|
return v;
|
|
150
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Migration helper — vault'tan oku, yoksa `process.env` fallback.
|
|
174
|
+
*
|
|
175
|
+
* Sentroy app'lerini kademeli olarak `process.env` → vault'a çevirirken
|
|
176
|
+
* "her ikisi de çalışsın" senaryosu için. Vault doldurulmamış / token
|
|
177
|
+
* eksik / fetch fail dönerse sessizce `process.env[key]`'e döner — eski
|
|
178
|
+
* deploy ile yeni kod bir arada çalışabilir.
|
|
179
|
+
*
|
|
180
|
+
* **Migration tamamlandıktan sonra** çağrı sitelerini `getEnv()` ya da
|
|
181
|
+
* `getEnvOrThrow()`'a çevir; fallback'i bırakmak silently process.env
|
|
182
|
+
* sızıntısı riskini taşır (kullanıcı vault'tan key'i sildi sansa bile
|
|
183
|
+
* eski process.env değeri etkili olur).
|
|
184
|
+
*
|
|
185
|
+
* Bootstrap path için (`SENTROY_ENV_API_KEY` set değil) doğrudan
|
|
186
|
+
* `process.env`'e döner — vault fetch denemez. Bu önemli: Sentroy app'i
|
|
187
|
+
* vault'sız boot edilebilir.
|
|
188
|
+
*/
|
|
189
|
+
async function getEnvWithFallback(key) {
|
|
190
|
+
// Token yoksa bypass — vault fetch denemeyelim, log spam etmeyelim.
|
|
191
|
+
const apiKey = resolvedApiKey ?? readEnv("SENTROY_ENV_API_KEY");
|
|
192
|
+
if (!apiKey) {
|
|
193
|
+
const pe = readEnv(key);
|
|
194
|
+
debugLog(`${key}: no-token bypass → ${pe !== undefined ? "process.env hit" : "miss"}`);
|
|
195
|
+
return pe;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const v = await getEnv(key);
|
|
199
|
+
if (v !== undefined) {
|
|
200
|
+
debugLog(`${key}: vault hit`);
|
|
201
|
+
return v;
|
|
202
|
+
}
|
|
203
|
+
const pe = readEnv(key);
|
|
204
|
+
debugLog(`${key}: vault miss → ${pe !== undefined ? "process.env fallback" : "undefined"}`);
|
|
205
|
+
return pe;
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
const pe = readEnv(key);
|
|
209
|
+
debugLog(`${key}: vault error (${err instanceof Error ? err.message : String(err)}) → ${pe !== undefined ? "process.env fallback" : "undefined"}`);
|
|
210
|
+
return pe;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
151
213
|
/** Tüm env'leri map olarak döner (dump için kullanışlı). */
|
|
152
214
|
async function getAllEnvs() {
|
|
153
215
|
const c = await ensureCache();
|
|
@@ -166,4 +228,86 @@ async function getPublicEnvs() {
|
|
|
166
228
|
}
|
|
167
229
|
return out;
|
|
168
230
|
}
|
|
231
|
+
const DEFAULT_MAX_AGE_MS = 5 * 60 * 1000;
|
|
232
|
+
async function timingSafeEqualHex(a, b) {
|
|
233
|
+
if (a.length !== b.length)
|
|
234
|
+
return false;
|
|
235
|
+
let diff = 0;
|
|
236
|
+
for (let i = 0; i < a.length; i++) {
|
|
237
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
238
|
+
}
|
|
239
|
+
return diff === 0;
|
|
240
|
+
}
|
|
241
|
+
async function hmacSha256Hex(secret, body) {
|
|
242
|
+
// Web Crypto — Node 18+ + browser ikisi de destekler.
|
|
243
|
+
const encoder = new TextEncoder();
|
|
244
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
245
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
246
|
+
const bytes = new Uint8Array(sig);
|
|
247
|
+
let hex = "";
|
|
248
|
+
for (const b of bytes)
|
|
249
|
+
hex += b.toString(16).padStart(2, "0");
|
|
250
|
+
return hex;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Bir Sentroy vault webhook receiver'ı için Request → Response handler
|
|
254
|
+
* üretir. Next.js App Router'da:
|
|
255
|
+
*
|
|
256
|
+
* // app/api/sentroy/vault-webhook/route.ts
|
|
257
|
+
* import { createVaultWebhookHandler } from "@sentroy-co/client-sdk/vault"
|
|
258
|
+
* export const POST = createVaultWebhookHandler({
|
|
259
|
+
* secret: process.env.SENTROY_VAULT_WEBHOOK_SECRET!,
|
|
260
|
+
* })
|
|
261
|
+
*
|
|
262
|
+
* Default davranış: imza doğruysa cache'i invalidate eder ve 200 döner.
|
|
263
|
+
* Hatalı/eksik imza → 401, eski timestamp → 401, body parse hatası → 400.
|
|
264
|
+
*/
|
|
265
|
+
function createVaultWebhookHandler(options) {
|
|
266
|
+
const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
267
|
+
return async (request) => {
|
|
268
|
+
const sigHeader = request.headers.get("x-sentroy-signature") || "";
|
|
269
|
+
const match = sigHeader.match(/^sha256=([a-f0-9]+)$/i);
|
|
270
|
+
if (!match) {
|
|
271
|
+
return new Response("missing or malformed X-Sentroy-Signature header", {
|
|
272
|
+
status: 401,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const providedSig = match[1].toLowerCase();
|
|
276
|
+
const body = await request.text();
|
|
277
|
+
const expected = await hmacSha256Hex(options.secret, body);
|
|
278
|
+
if (!(await timingSafeEqualHex(providedSig, expected))) {
|
|
279
|
+
return new Response("signature mismatch", { status: 401 });
|
|
280
|
+
}
|
|
281
|
+
let payload;
|
|
282
|
+
try {
|
|
283
|
+
payload = JSON.parse(body);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return new Response("invalid JSON body", { status: 400 });
|
|
287
|
+
}
|
|
288
|
+
if (maxAgeMs > 0) {
|
|
289
|
+
const age = Date.now() - (payload.timestamp ?? 0);
|
|
290
|
+
if (!Number.isFinite(age) || age < 0 || age > maxAgeMs) {
|
|
291
|
+
return new Response("payload timestamp outside acceptable window", {
|
|
292
|
+
status: 401,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
if (options.onChange) {
|
|
298
|
+
await options.onChange(payload);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
await refreshEnvCache();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
return new Response(`handler error: ${err instanceof Error ? err.message : String(err)}`, { status: 500 });
|
|
306
|
+
}
|
|
307
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
308
|
+
status: 200,
|
|
309
|
+
headers: { "Content-Type": "application/json" },
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
}
|
|
169
313
|
//# sourceMappingURL=index.js.map
|
package/dist/vault/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vault/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vault/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;AA+DH,gDAYC;AAGD,wCAEC;AAGD,0CAGC;AAGD,gCAEC;AAwED,wBAGC;AAGD,sCAQC;AAmBD,gDA0BC;AAGD,gCAKC;AAGD,sCAOC;AA2ED,8DAqDC;AAhWD,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;AACpC,MAAM,gBAAgB,GAAG,qBAAqB,CAAA;AAE9C;;;;GAIG;AACH,SAAS,OAAO;IACd,MAAM,CAAC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACtC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,MAAM,CAAA;AAClC,CAAC;AAED,SAAS,QAAQ,CAAC,GAAG,KAAgB;IACnC,IAAI,OAAO,EAAE,EAAE,CAAC;QACd,sCAAsC;QACtC,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,GAAG,KAAK,CAAC,CAAA;IACtC,CAAC;AACH,CAAC;AAaD,IAAI,eAAe,GAAG,gBAAgB,CAAA;AACtC,IAAI,cAAkC,CAAA;AACtC,IAAI,UAAU,GAAG,cAAc,CAAA;AAC/B,IAAI,cAAc,GAAG,IAAI,CAAA;AACzB,IAAI,KAAK,GAAyB,IAAI,CAAA;AACtC,IAAI,cAAc,GAAyB,IAAI,CAAA;AAE/C,SAAS,OAAO,CAAC,IAAY;IAC3B,IAAI,OAAO,OAAO,KAAK,WAAW;QAAE,OAAO,SAAS,CAAA;IACpD,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;AAC5B,CAAC;AAED;;;GAGG;AACH,SAAgB,kBAAkB,CAAC,UAAyB,EAAE;IAC5D,IAAI,OAAO,CAAC,OAAO;QAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;;QAExE,eAAe,GAAG,CAChB,OAAO,CAAC,iCAAiC,CAAC;YAC1C,OAAO,CAAC,qBAAqB,CAAC;YAC9B,OAAO,CAAC,0BAA0B,CAAC;YACnC,gBAAgB,CACjB,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACvB,cAAc,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACjE,IAAI,OAAO,CAAC,UAAU;QAAE,UAAU,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAA;IAC9D,IAAI,OAAO,CAAC,SAAS;QAAE,cAAc,GAAG,OAAO,CAAC,SAAS,CAAA;AAC3D,CAAC;AAED,8EAA8E;AAC9E,SAAgB,cAAc,CAAC,OAAe;IAC5C,UAAU,GAAG,OAAO,GAAG,IAAI,CAAA;AAC7B,CAAC;AAED,8EAA8E;AACvE,KAAK,UAAU,eAAe;IACnC,KAAK,GAAG,IAAI,CAAA;IACZ,MAAM,WAAW,EAAE,CAAA;AACrB,CAAC;AAED,2EAA2E;AACpE,KAAK,UAAU,UAAU;IAC9B,MAAM,WAAW,EAAE,CAAA;AACrB,CAAC;AAED,KAAK,UAAU,cAAc;IAC3B,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,iEAAiE;QACjE,kBAAkB,EAAE,CAAA;IACtB,CAAC;IACD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,gEAAgE;YAC9D,2FAA2F,CAC9F,CAAA;IACH,CAAC;IACD,MAAM,GAAG,GAAG,GAAG,eAAe,sBAAsB,CAAA;IACpD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IAC1B,QAAQ,CAAC,YAAY,GAAG,EAAE,CAAC,CAAA;IAC3B,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC3B,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,cAAc,EAAE,EAAE;QACtD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC;QAC3C,KAAK,EAAE,UAAU;KAClB,CAAC,CAAA;IACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,QAAQ,CAAC,iBAAiB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,KAAK,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,KAAK,CAAC,CAAA;QACrF,MAAM,IAAI,KAAK,CACb,2BAA2B,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,SAAS,GAAG,GAAG,CACvE,CAAA;IACH,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAM7B,CAAA;IACD,IAAI,CAAC,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;IACtE,MAAM,GAAG,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC1C,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS;QAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;IACtD,QAAQ,CACN,WAAW,GAAG,CAAC,IAAI,gBAAgB,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,IAAI,CAC7G,CAAA;IACD,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,SAAS,EAAE,GAAG;QACd,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO;QAC1B,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW;KACnC,CAAA;AACH,CAAC;AAED,KAAK,UAAU,WAAW;IACxB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,IAAI,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC,SAAS,GAAG,UAAU;QAAE,OAAO,KAAK,CAAA;IAC7D,IAAI,cAAc,EAAE,CAAC;QACnB,MAAM,cAAc,CAAA;QACpB,IAAI,KAAK;YAAE,OAAO,KAAK,CAAA;IACzB,CAAC;IACD,cAAc,GAAG,CAAC,KAAK,IAAI,EAAE;QAC3B,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,cAAc,EAAE,CAAA;QAChC,CAAC;gBAAS,CAAC;YACT,cAAc,GAAG,IAAI,CAAA;QACvB,CAAC;IACH,CAAC,CAAC,EAAE,CAAA;IACJ,MAAM,cAAc,CAAA;IACpB,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IAC9D,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;GAIG;AACI,KAAK,UAAU,MAAM,CAAC,GAAW;IACtC,MAAM,CAAC,GAAG,MAAM,WAAW,EAAE,CAAA;IAC7B,OAAO,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,CAAA;AACpC,CAAC;AAED,8EAA8E;AACvE,KAAK,UAAU,aAAa,CAAC,GAAW;IAC7C,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,CAAA;IAC3B,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,gCAAgC,GAAG,4BAA4B,KAAK,EAAE,OAAO,IAAI,GAAG,SAAS,KAAK,EAAE,WAAW,IAAI,GAAG,GAAG,CAC1H,CAAA;IACH,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACI,KAAK,UAAU,kBAAkB,CACtC,GAAW;IAEX,oEAAoE;IACpE,MAAM,MAAM,GAAG,cAAc,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC/D,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;QACvB,QAAQ,CAAC,GAAG,GAAG,uBAAuB,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;QACtF,OAAO,EAAE,CAAA;IACX,CAAC;IACD,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,CAAA;QAC3B,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;YACpB,QAAQ,CAAC,GAAG,GAAG,aAAa,CAAC,CAAA;YAC7B,OAAO,CAAC,CAAA;QACV,CAAC;QACD,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;QACvB,QAAQ,CAAC,GAAG,GAAG,kBAAkB,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAA;QAC3F,OAAO,EAAE,CAAA;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;QACvB,QAAQ,CACN,GAAG,GAAG,kBAAkB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,WAAW,EAAE,CACzI,CAAA;QACD,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,4DAA4D;AACrD,KAAK,UAAU,UAAU;IAC9B,MAAM,CAAC,GAAG,MAAM,WAAW,EAAE,CAAA;IAC7B,MAAM,GAAG,GAA2B,EAAE,CAAA;IACtC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;IAClD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,gEAAgE;AACzD,KAAK,UAAU,aAAa;IACjC,MAAM,CAAC,GAAG,MAAM,WAAW,EAAE,CAAA;IAC7B,MAAM,GAAG,GAA2B,EAAE,CAAA;IACtC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC,CAAC,MAAM;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;IAChC,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAkCD,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;AAExC,KAAK,UAAU,kBAAkB,CAAC,CAAS,EAAE,CAAS;IACpD,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACvC,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAC3C,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAA;AACnB,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,IAAY;IACvD,sDAAsD;IACtD,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;IACjC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACvC,KAAK,EACL,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EACtB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAA;IACD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IACvE,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAA;IACjC,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC7D,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAgB,yBAAyB,CACvC,OAAyC;IAEzC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,kBAAkB,CAAA;IACvD,OAAO,KAAK,EAAE,OAAgB,EAAE,EAAE;QAChC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,IAAI,EAAE,CAAA;QAClE,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAA;QACtD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,QAAQ,CAAC,iDAAiD,EAAE;gBACrE,MAAM,EAAE,GAAG;aACZ,CAAC,CAAA;QACJ,CAAC;QACD,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;QAC1C,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAA;QACjC,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;QAC1D,IAAI,CAAC,CAAC,MAAM,kBAAkB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;YACvD,OAAO,IAAI,QAAQ,CAAC,oBAAoB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QAC5D,CAAC;QAED,IAAI,OAA4B,CAAA;QAChC,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAwB,CAAA;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,QAAQ,CAAC,mBAAmB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QAC3D,CAAC;QAED,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YACjB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC,CAAA;YACjD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,QAAQ,EAAE,CAAC;gBACvD,OAAO,IAAI,QAAQ,CAAC,6CAA6C,EAAE;oBACjE,MAAM,EAAE,GAAG;iBACZ,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAED,IAAI,CAAC;YACH,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACrB,MAAM,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;YACjC,CAAC;iBAAM,CAAC;gBACN,MAAM,eAAe,EAAE,CAAA;YACzB,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,IAAI,QAAQ,CACjB,kBAAkB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EACpE,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAA;QACH,CAAC;QAED,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;YAChD,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
package/src/vault/index.ts
CHANGED
|
@@ -42,6 +42,23 @@ export interface EnvCacheState {
|
|
|
42
42
|
const DEFAULT_TTL_MS = 5 * 60 * 1000
|
|
43
43
|
const DEFAULT_BASE_URL = "https://sentroy.com"
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Opt-in runtime logging — `SENTROY_ENV_DEBUG=1` set edilirse her fetch
|
|
47
|
+
* (success/fail), cache hit ve fallback satırı stdout'a yazılır. Default
|
|
48
|
+
* kapalı; migration verification + prod sorunu debug için.
|
|
49
|
+
*/
|
|
50
|
+
function isDebug(): boolean {
|
|
51
|
+
const v = readEnv("SENTROY_ENV_DEBUG")
|
|
52
|
+
return v === "1" || v === "true"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function debugLog(...parts: unknown[]): void {
|
|
56
|
+
if (isDebug()) {
|
|
57
|
+
// eslint-disable-next-line no-console
|
|
58
|
+
console.log("[env-vault]", ...parts)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
interface ClientOptions {
|
|
46
63
|
/** Sentroy core URL (defaults to env or https://sentroy.com). */
|
|
47
64
|
baseUrl?: string
|
|
@@ -111,12 +128,15 @@ async function fetchVariables(): Promise<EnvCacheState> {
|
|
|
111
128
|
)
|
|
112
129
|
}
|
|
113
130
|
const url = `${resolvedBaseUrl}/api/env-vault/fetch`
|
|
131
|
+
const started = Date.now()
|
|
132
|
+
debugLog(`fetching ${url}`)
|
|
114
133
|
const res = await fetch(url, {
|
|
115
134
|
headers: { Authorization: `Bearer ${resolvedApiKey}` },
|
|
116
135
|
signal: AbortSignal.timeout(fetchTimeoutMs),
|
|
117
136
|
cache: "no-store",
|
|
118
137
|
})
|
|
119
138
|
if (!res.ok) {
|
|
139
|
+
debugLog(`fetch failed: ${res.status} ${res.statusText} (${Date.now() - started}ms)`)
|
|
120
140
|
throw new Error(
|
|
121
141
|
`env-vault fetch failed: ${res.status} ${res.statusText} (url=${url})`,
|
|
122
142
|
)
|
|
@@ -131,6 +151,9 @@ async function fetchVariables(): Promise<EnvCacheState> {
|
|
|
131
151
|
if (!json.data) throw new Error("env-vault fetch: malformed response")
|
|
132
152
|
const map = new Map<string, EnvVariable>()
|
|
133
153
|
for (const v of json.data.variables) map.set(v.key, v)
|
|
154
|
+
debugLog(
|
|
155
|
+
`fetched ${map.size} var(s) from ${json.data.project}/${json.data.environment} in ${Date.now() - started}ms`,
|
|
156
|
+
)
|
|
134
157
|
return {
|
|
135
158
|
fetchedAt: Date.now(),
|
|
136
159
|
variables: map,
|
|
@@ -179,6 +202,51 @@ export async function getEnvOrThrow(key: string): Promise<string> {
|
|
|
179
202
|
return v
|
|
180
203
|
}
|
|
181
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Migration helper — vault'tan oku, yoksa `process.env` fallback.
|
|
207
|
+
*
|
|
208
|
+
* Sentroy app'lerini kademeli olarak `process.env` → vault'a çevirirken
|
|
209
|
+
* "her ikisi de çalışsın" senaryosu için. Vault doldurulmamış / token
|
|
210
|
+
* eksik / fetch fail dönerse sessizce `process.env[key]`'e döner — eski
|
|
211
|
+
* deploy ile yeni kod bir arada çalışabilir.
|
|
212
|
+
*
|
|
213
|
+
* **Migration tamamlandıktan sonra** çağrı sitelerini `getEnv()` ya da
|
|
214
|
+
* `getEnvOrThrow()`'a çevir; fallback'i bırakmak silently process.env
|
|
215
|
+
* sızıntısı riskini taşır (kullanıcı vault'tan key'i sildi sansa bile
|
|
216
|
+
* eski process.env değeri etkili olur).
|
|
217
|
+
*
|
|
218
|
+
* Bootstrap path için (`SENTROY_ENV_API_KEY` set değil) doğrudan
|
|
219
|
+
* `process.env`'e döner — vault fetch denemez. Bu önemli: Sentroy app'i
|
|
220
|
+
* vault'sız boot edilebilir.
|
|
221
|
+
*/
|
|
222
|
+
export async function getEnvWithFallback(
|
|
223
|
+
key: string,
|
|
224
|
+
): Promise<string | undefined> {
|
|
225
|
+
// Token yoksa bypass — vault fetch denemeyelim, log spam etmeyelim.
|
|
226
|
+
const apiKey = resolvedApiKey ?? readEnv("SENTROY_ENV_API_KEY")
|
|
227
|
+
if (!apiKey) {
|
|
228
|
+
const pe = readEnv(key)
|
|
229
|
+
debugLog(`${key}: no-token bypass → ${pe !== undefined ? "process.env hit" : "miss"}`)
|
|
230
|
+
return pe
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const v = await getEnv(key)
|
|
234
|
+
if (v !== undefined) {
|
|
235
|
+
debugLog(`${key}: vault hit`)
|
|
236
|
+
return v
|
|
237
|
+
}
|
|
238
|
+
const pe = readEnv(key)
|
|
239
|
+
debugLog(`${key}: vault miss → ${pe !== undefined ? "process.env fallback" : "undefined"}`)
|
|
240
|
+
return pe
|
|
241
|
+
} catch (err) {
|
|
242
|
+
const pe = readEnv(key)
|
|
243
|
+
debugLog(
|
|
244
|
+
`${key}: vault error (${err instanceof Error ? err.message : String(err)}) → ${pe !== undefined ? "process.env fallback" : "undefined"}`,
|
|
245
|
+
)
|
|
246
|
+
return pe
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
182
250
|
/** Tüm env'leri map olarak döner (dump için kullanışlı). */
|
|
183
251
|
export async function getAllEnvs(): Promise<Record<string, string>> {
|
|
184
252
|
const c = await ensureCache()
|
|
@@ -196,3 +264,131 @@ export async function getPublicEnvs(): Promise<Record<string, string>> {
|
|
|
196
264
|
}
|
|
197
265
|
return out
|
|
198
266
|
}
|
|
267
|
+
|
|
268
|
+
// ── Webhook handler ─────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
export interface VaultWebhookPayload {
|
|
271
|
+
event: "vault.variable.changed"
|
|
272
|
+
project: string
|
|
273
|
+
environment: string
|
|
274
|
+
action: "create" | "update" | "delete"
|
|
275
|
+
/** Etkilenen key'ler — bulk push'ta birden fazla. */
|
|
276
|
+
keys: string[]
|
|
277
|
+
/** Unix ms. */
|
|
278
|
+
timestamp: number
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface CreateVaultWebhookHandlerOptions {
|
|
282
|
+
/**
|
|
283
|
+
* Sentroy vault dashboard'dan aldığın webhook secret (`whsec_...`).
|
|
284
|
+
* Receiver bu secret'la HMAC-SHA256 imzayı doğrular; hatalıysa 401 döner.
|
|
285
|
+
*/
|
|
286
|
+
secret: string
|
|
287
|
+
/**
|
|
288
|
+
* Imzayı doğruladıktan sonra çağrılır. Default davranış:
|
|
289
|
+
* `await refreshEnvCache()` — bir sonraki getEnv() taze değerleri çeker.
|
|
290
|
+
* Custom logic için override et (örn. tek bir key'i targeted invalidate).
|
|
291
|
+
*/
|
|
292
|
+
onChange?: (payload: VaultWebhookPayload) => Promise<void> | void
|
|
293
|
+
/**
|
|
294
|
+
* Replay attack'lere karşı body'nin timestamp'i ile şu an arasındaki
|
|
295
|
+
* maksimum tolerans (ms). Default 5 dk. Sıfır ise check kapalı.
|
|
296
|
+
*/
|
|
297
|
+
maxAgeMs?: number
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const DEFAULT_MAX_AGE_MS = 5 * 60 * 1000
|
|
301
|
+
|
|
302
|
+
async function timingSafeEqualHex(a: string, b: string): Promise<boolean> {
|
|
303
|
+
if (a.length !== b.length) return false
|
|
304
|
+
let diff = 0
|
|
305
|
+
for (let i = 0; i < a.length; i++) {
|
|
306
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
307
|
+
}
|
|
308
|
+
return diff === 0
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function hmacSha256Hex(secret: string, body: string): Promise<string> {
|
|
312
|
+
// Web Crypto — Node 18+ + browser ikisi de destekler.
|
|
313
|
+
const encoder = new TextEncoder()
|
|
314
|
+
const key = await crypto.subtle.importKey(
|
|
315
|
+
"raw",
|
|
316
|
+
encoder.encode(secret),
|
|
317
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
318
|
+
false,
|
|
319
|
+
["sign"],
|
|
320
|
+
)
|
|
321
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body))
|
|
322
|
+
const bytes = new Uint8Array(sig)
|
|
323
|
+
let hex = ""
|
|
324
|
+
for (const b of bytes) hex += b.toString(16).padStart(2, "0")
|
|
325
|
+
return hex
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Bir Sentroy vault webhook receiver'ı için Request → Response handler
|
|
330
|
+
* üretir. Next.js App Router'da:
|
|
331
|
+
*
|
|
332
|
+
* // app/api/sentroy/vault-webhook/route.ts
|
|
333
|
+
* import { createVaultWebhookHandler } from "@sentroy-co/client-sdk/vault"
|
|
334
|
+
* export const POST = createVaultWebhookHandler({
|
|
335
|
+
* secret: process.env.SENTROY_VAULT_WEBHOOK_SECRET!,
|
|
336
|
+
* })
|
|
337
|
+
*
|
|
338
|
+
* Default davranış: imza doğruysa cache'i invalidate eder ve 200 döner.
|
|
339
|
+
* Hatalı/eksik imza → 401, eski timestamp → 401, body parse hatası → 400.
|
|
340
|
+
*/
|
|
341
|
+
export function createVaultWebhookHandler(
|
|
342
|
+
options: CreateVaultWebhookHandlerOptions,
|
|
343
|
+
): (request: Request) => Promise<Response> {
|
|
344
|
+
const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS
|
|
345
|
+
return async (request: Request) => {
|
|
346
|
+
const sigHeader = request.headers.get("x-sentroy-signature") || ""
|
|
347
|
+
const match = sigHeader.match(/^sha256=([a-f0-9]+)$/i)
|
|
348
|
+
if (!match) {
|
|
349
|
+
return new Response("missing or malformed X-Sentroy-Signature header", {
|
|
350
|
+
status: 401,
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
const providedSig = match[1].toLowerCase()
|
|
354
|
+
const body = await request.text()
|
|
355
|
+
const expected = await hmacSha256Hex(options.secret, body)
|
|
356
|
+
if (!(await timingSafeEqualHex(providedSig, expected))) {
|
|
357
|
+
return new Response("signature mismatch", { status: 401 })
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let payload: VaultWebhookPayload
|
|
361
|
+
try {
|
|
362
|
+
payload = JSON.parse(body) as VaultWebhookPayload
|
|
363
|
+
} catch {
|
|
364
|
+
return new Response("invalid JSON body", { status: 400 })
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (maxAgeMs > 0) {
|
|
368
|
+
const age = Date.now() - (payload.timestamp ?? 0)
|
|
369
|
+
if (!Number.isFinite(age) || age < 0 || age > maxAgeMs) {
|
|
370
|
+
return new Response("payload timestamp outside acceptable window", {
|
|
371
|
+
status: 401,
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
if (options.onChange) {
|
|
378
|
+
await options.onChange(payload)
|
|
379
|
+
} else {
|
|
380
|
+
await refreshEnvCache()
|
|
381
|
+
}
|
|
382
|
+
} catch (err) {
|
|
383
|
+
return new Response(
|
|
384
|
+
`handler error: ${err instanceof Error ? err.message : String(err)}`,
|
|
385
|
+
{ status: 500 },
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
390
|
+
status: 200,
|
|
391
|
+
headers: { "Content-Type": "application/json" },
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
}
|