jskelet 0.3.2 → 0.3.3

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/CHANGELOG.md CHANGED
@@ -30,9 +30,12 @@ one is listed under a **Breaking** heading.
30
30
  - Top-level `logs` config for persistent sinks: daily NDJSON files
31
31
  (`logs.file`) and batched S3 PutObject (`logs.s3`) with embedded SigV4 — no
32
32
  `@aws-sdk` dependency. `kinds` selects `http` / `event` / `error`; `console`
33
- toggles runtime stdout lines. Missing bucket/region/credentials warn and
33
+ toggles runtime stdout lines. `JSKELET_LOG_BUCKET` (and `logs.s3.bucket`) may
34
+ be a plain bucket or a `bucket/prefix/…` path. `JSKELET_S3_API_URL` sets the
35
+ R2/MinIO endpoint (region defaults to `auto`). Missing credentials warn and
34
36
  disable the S3 sink without taking the site down. Env: `JSKELET_LOG_BUCKET`,
35
- `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`.
37
+ `JSKELET_S3_ACCESS_KEY_ID`, `JSKELET_S3_SECRET_ACCESS_KEY`,
38
+ `JSKELET_S3_SESSION_TOKEN`, `JSKELET_S3_REGION`, `JSKELET_S3_API_URL`.
36
39
  - Admin panel pages under `/_jskelet/admin`: Overview, Cache, Routes, Views,
37
40
  Logs and System. Configurable `allowIps` (exact or CIDR), `blockBots` (default
38
41
  on — crawler UAs get 404 before login), and `logSize`. Live Logs use an
@@ -766,17 +766,18 @@ mevcut davranışını korur. Açıldığında HTTP access log ile framework ola
766
766
  | `file.dir` | `string` | `"logs"` | Proje köküne göre dizin; `jskelet-YYYY-MM-DD.log` |
767
767
  | `file.rotate` | `"daily"` | `"daily"` | Yalnızca günlük rotasyon |
768
768
  | `s3.enabled` | `boolean` | `false` | S3 batch PutObject sink'i |
769
- | `s3.bucket` | `string \| null` | `null` | Bucket; `JSKELET_LOG_BUCKET` ezer |
770
- | `s3.prefix` | `string` | `"jskelet/logs/"` | Nesne anahtarı öneki |
771
- | `s3.region` | `string \| null` | `null` | Bölge; verilmezse `AWS_REGION` / `AWS_DEFAULT_REGION` |
772
- | `s3.endpoint` | `string \| null` | `null` | MinIO vb. için path-style endpoint |
769
+ | `s3.bucket` | `string \| null` | `null` | Bucket ya da `bucket/prefix/…` yolu; `JSKELET_LOG_BUCKET` ezer |
770
+ | `s3.prefix` | `string` | `"jskelet/logs/"` | Nesne anahtarı öneki (yolda verilmediyse) |
771
+ | `s3.region` | `string \| null` | `null` | Bölge; verilmezse `JSKELET_S3_REGION`, endpoint varken `auto` |
772
+ | `s3.endpoint` | `string \| null` | `null` | R2 / MinIO API adresi; `JSKELET_S3_API_URL` ezer |
773
773
  | `s3.flushIntervalMs` | `number` | `5000` | Batch flush aralığı |
774
774
  | `s3.maxBatch` | `number` | `100` | Bu kadar satırda erken flush |
775
775
 
776
- S3 credential'ları config'e yazılmaz: `AWS_ACCESS_KEY_ID`,
777
- `AWS_SECRET_ACCESS_KEY`, isteğe bağlı `AWS_SESSION_TOKEN`. Bucket/region/
778
- credential eksikse uyarı basılır ve S3 sink kapanır; site ayağa kalkmaya devam
779
- eder. Framework `@aws-sdk` taşımaz — PutObject SigV4 ile gömülüdür.
776
+ S3 credential'ları config'e yazılmaz: `JSKELET_S3_ACCESS_KEY_ID`,
777
+ `JSKELET_S3_SECRET_ACCESS_KEY`, isteğe bağlı `JSKELET_S3_SESSION_TOKEN`.
778
+ Bucket/region/credential eksikse uyarı basılır ve S3 sink kapanır; site ayağa
779
+ kalkmaya devam eder. Framework `@aws-sdk` taşımaz — PutObject SigV4 ile
780
+ gömülüdür.
780
781
 
781
782
  ```js
782
783
  logs: {
@@ -787,7 +788,8 @@ logs: {
787
788
  enabled: process.env.NODE_ENV === "production",
788
789
  bucket: process.env.JSKELET_LOG_BUCKET,
789
790
  prefix: "my-app/logs/",
790
- region: process.env.AWS_REGION,
791
+ region: process.env.JSKELET_S3_REGION,
792
+ endpoint: process.env.JSKELET_S3_API_URL,
791
793
  },
792
794
  }
793
795
  ```
@@ -973,11 +975,12 @@ basılmaz.
973
975
  | `JSKELET_SECRET` | `jskelet/cookies` | — | İmzalı cookie sırrı. `security.cookieSecret` verilmediğinde buradan okunur; ikisi de yoksa imzalı cookie API'si hata verir. [12](./12-panel-ve-oturum.md) |
974
976
  | `DEV_TOKEN` | `devGate`, `prewarm` | — | Ayarlıysa token taşımayan her isteğe 404 döner. Isıtma token'ı çerez olarak taşır. [09](./09-dev-araclari.md) |
975
977
  | `JSKELET_ADMIN` | `createApp` | — | Ayarlıysa yönetim panelini açar; `0` config'te açık olan paneli kapatır. Env config'i ezer, çünkü panel genelde bir arıza sırasında tek seferlik açılır. [06](./06-cache.md) |
976
- | `JSKELET_LOG_BUCKET` | `logs.s3` | — | S3 log bucket'ı; `logs.s3.bucket`'ı ezer |
977
- | `AWS_ACCESS_KEY_ID` | `logs.s3` | — | S3 PutObject imzası. Yoksa ve `s3.enabled` ise sink kapanır |
978
- | `AWS_SECRET_ACCESS_KEY` | `logs.s3` | — | S3 imza sırrı |
979
- | `AWS_SESSION_TOKEN` | `logs.s3` | — | Geçici credential'lar için isteğe bağlı |
980
- | `AWS_REGION` / `AWS_DEFAULT_REGION` | `logs.s3` | — | `logs.s3.region` verilmezse buradan okunur |
978
+ | `JSKELET_LOG_BUCKET` | `logs.s3` | — | S3 bucket ya da `ayberkenis/jskelet/logs` gibi `bucket/prefix` yolu; `logs.s3.bucket`'ı ezer (`JSKELET_S3_BUCKET` yedek ad) |
979
+ | `JSKELET_S3_ACCESS_KEY_ID` | `logs.s3` | — | S3 PutObject imzası. Yoksa ve `s3.enabled` ise sink kapanır |
980
+ | `JSKELET_S3_SECRET_ACCESS_KEY` | `logs.s3` | — | S3 imza sırrı |
981
+ | `JSKELET_S3_SESSION_TOKEN` | `logs.s3` | — | Geçici credential'lar için isteğe bağlı |
982
+ | `JSKELET_S3_REGION` | `logs.s3` | — | `logs.s3.region` verilmezse; endpoint varken varsayılan `auto` |
983
+ | `JSKELET_S3_API_URL` | `logs.s3` | — | R2 / MinIO endpoint; `logs.s3.endpoint`'i ezer |
981
984
  | `JSKELET_CLOUDFLARE_KEY` | Cloudflare cache yüzeyi | — | API token. Verilene kadar CDN purge'ü ve edge analitiği kapalıdır; config'teki `apiToken`'ı ezer. Token hiçbir cevapta dönmez. [06](./06-cache.md) |
982
985
  | `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache yüzeyi | — | Zone kimliği. Token'la birlikte verilmedikçe hiçbir Cloudflare ucu çağrılmaz |
983
986
  | `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache yüzeyi | — | Purge URL'lerinin kökü. Panel iç bir adresten açılıyorsa gerekir |
@@ -47,7 +47,7 @@ ayarlamayı düşünmeniz gerekenler:
47
47
  | `PREWARM_MAX` | Site boyutuna göre | Açılışta ısıtılacak sayfa sayısı |
48
48
  | `PREWARM_INTERVAL_SECONDS` | `0` ya da uzun bir değer | Hiç ziyaret edilmeyen sayfaları sıcak tutmak isterseniz |
49
49
  | `DEV_TOKEN` | Yalnızca staging'de | Yayına açılmamış ortamı gizler |
50
- | `JSKELET_LOG_BUCKET` + `AWS_*` | Access log'u S3'e yazıyorsanız | `logs.s3` credential ve bucket; ayrıntı [07](./07-yapilandirma.md) |
50
+ | `JSKELET_S3_*` | Access log'u S3'e yazıyorsanız | Bucket + credential; ayrıntı [07](./07-yapilandirma.md) |
51
51
 
52
52
  Production'da dosya veya S3 sink açıldığında HTTP access log middleware
53
53
  otomatik mount edilir (`logs.kinds` içinde `http` varsa). Admin paneli ring'i
@@ -782,17 +782,17 @@ events (`event` / `error`) are written as NDJSON lines to a file and/or S3.
782
782
  | `file.dir` | `string` | `"logs"` | Directory relative to the project root; `jskelet-YYYY-MM-DD.log` |
783
783
  | `file.rotate` | `"daily"` | `"daily"` | Daily rotation only |
784
784
  | `s3.enabled` | `boolean` | `false` | S3 batch PutObject sink |
785
- | `s3.bucket` | `string \| null` | `null` | Bucket; `JSKELET_LOG_BUCKET` overrides |
786
- | `s3.prefix` | `string` | `"jskelet/logs/"` | Object key prefix |
787
- | `s3.region` | `string \| null` | `null` | Region; falls back to `AWS_REGION` / `AWS_DEFAULT_REGION` when unset |
788
- | `s3.endpoint` | `string \| null` | `null` | Path-style endpoint for MinIO etc. |
785
+ | `s3.bucket` | `string \| null` | `null` | Bucket or a `bucket/prefix/…` path; `JSKELET_LOG_BUCKET` overrides |
786
+ | `s3.prefix` | `string` | `"jskelet/logs/"` | Object key prefix (when not given in the path) |
787
+ | `s3.region` | `string \| null` | `null` | Region; falls back to `JSKELET_S3_REGION`, or `auto` when an endpoint is set |
788
+ | `s3.endpoint` | `string \| null` | `null` | R2 / MinIO API URL; `JSKELET_S3_API_URL` overrides |
789
789
  | `s3.flushIntervalMs` | `number` | `5000` | Batch flush interval |
790
790
  | `s3.maxBatch` | `number` | `100` | Flush early after this many lines |
791
791
 
792
- S3 credentials are not written in the config: `AWS_ACCESS_KEY_ID`,
793
- `AWS_SECRET_ACCESS_KEY`, optional `AWS_SESSION_TOKEN`. Missing bucket/region/
794
- credentials warn and disable the S3 sink; the site still starts. The framework
795
- does not ship `@aws-sdk` — PutObject is embedded with SigV4.
792
+ S3 credentials are not written in the config: `JSKELET_S3_ACCESS_KEY_ID`,
793
+ `JSKELET_S3_SECRET_ACCESS_KEY`, optional `JSKELET_S3_SESSION_TOKEN`. Missing
794
+ bucket/region/credentials warn and disable the S3 sink; the site still starts.
795
+ The framework does not ship `@aws-sdk` — PutObject is embedded with SigV4.
796
796
 
797
797
  ```js
798
798
  logs: {
@@ -803,7 +803,8 @@ logs: {
803
803
  enabled: process.env.NODE_ENV === "production",
804
804
  bucket: process.env.JSKELET_LOG_BUCKET,
805
805
  prefix: "my-app/logs/",
806
- region: process.env.AWS_REGION,
806
+ region: process.env.JSKELET_S3_REGION,
807
+ endpoint: process.env.JSKELET_S3_API_URL,
807
808
  },
808
809
  }
809
810
  ```
@@ -994,11 +995,12 @@ and no warning is printed.
994
995
  | `JSKELET_SECRET` | `jskelet/cookies` | — | The signed cookie secret. Read when `security.cookieSecret` is not set; if neither exists, the signed cookie API throws. [12](./12-dashboards-and-sessions.md) |
995
996
  | `DEV_TOKEN` | `devGate`, `prewarm` | — | If set, every request without a token gets a 404. Prewarming carries the token as a cookie. [09](./09-dev-tools.md) |
996
997
  | `JSKELET_ADMIN` | `createApp` | — | When set, turns the admin panel on; `0` turns off a panel enabled in the config. The env wins because the panel is usually opened once during an incident. [06](./06-caching.md) |
997
- | `JSKELET_LOG_BUCKET` | `logs.s3` | — | S3 log bucket; overrides `logs.s3.bucket` |
998
- | `AWS_ACCESS_KEY_ID` | `logs.s3` | — | Signs S3 PutObject. Missing when `s3.enabled` disables the sink |
999
- | `AWS_SECRET_ACCESS_KEY` | `logs.s3` | — | S3 signing secret |
1000
- | `AWS_SESSION_TOKEN` | `logs.s3` | — | Optional, for temporary credentials |
1001
- | `AWS_REGION` / `AWS_DEFAULT_REGION` | `logs.s3` | — | Read when `logs.s3.region` is unset |
998
+ | `JSKELET_LOG_BUCKET` | `logs.s3` | — | S3 bucket or a `bucket/prefix` path like `ayberkenis/jskelet/logs`; overrides `logs.s3.bucket` (`JSKELET_S3_BUCKET` is an alias) |
999
+ | `JSKELET_S3_ACCESS_KEY_ID` | `logs.s3` | — | Signs S3 PutObject. Missing when `s3.enabled` disables the sink |
1000
+ | `JSKELET_S3_SECRET_ACCESS_KEY` | `logs.s3` | — | S3 signing secret |
1001
+ | `JSKELET_S3_SESSION_TOKEN` | `logs.s3` | — | Optional, for temporary credentials |
1002
+ | `JSKELET_S3_REGION` | `logs.s3` | — | Used when `logs.s3.region` is unset; defaults to `auto` when an endpoint is set |
1003
+ | `JSKELET_S3_API_URL` | `logs.s3` | — | R2 / MinIO endpoint; overrides `logs.s3.endpoint` |
1002
1004
  | `JSKELET_CLOUDFLARE_KEY` | Cloudflare cache surface | — | API token. Until it is set, CDN purging and edge analytics stay off; it overrides `apiToken` in the config. The token is never returned in a response. [06](./06-caching.md) |
1003
1005
  | `JSKELET_CLOUDFLARE_ZONE_ID` | Cloudflare cache surface | — | Zone identifier. No Cloudflare endpoint is called unless it is set alongside the token |
1004
1006
  | `JSKELET_CLOUDFLARE_HOSTNAME` | Cloudflare cache surface | — | The root for purge URLs. Required when the panel is opened over an internal address |
@@ -48,7 +48,7 @@ considering in production:
48
48
  | `PREWARM_MAX` | Depends on site size | Number of pages warmed at startup |
49
49
  | `PREWARM_INTERVAL_SECONDS` | `0` or a long value | If you want to keep never-visited pages warm |
50
50
  | `DEV_TOKEN` | Staging only | Hides an environment that is not public yet |
51
- | `JSKELET_LOG_BUCKET` + `AWS_*` | If you write access logs to S3 | `logs.s3` credentials and bucket; details in [07](./07-configuration.md) |
51
+ | `JSKELET_S3_*` | If you write access logs to S3 | Bucket + credentials; details in [07](./07-configuration.md) |
52
52
 
53
53
  When a file or S3 sink is enabled in production, the HTTP access log middleware
54
54
  mounts automatically (if `http` is in `logs.kinds`). The admin panel ring is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "A framework that feels like no framework: Express 5 + EJS server rendering, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -199,9 +199,11 @@ export const DEFAULT_REDIS = {
199
199
  * runtime `http`/`event`/`error` satırlarının terminalde görünmesini kontrol
200
200
  * eder (banner/build satırlarına dokunmaz).
201
201
  *
202
- * S3 credential'ları config'e yazılmaz — `AWS_ACCESS_KEY_ID` /
203
- * `AWS_SECRET_ACCESS_KEY` (ve isteğe bağlı `AWS_SESSION_TOKEN`). Bucket için
204
- * `JSKELET_LOG_BUCKET` env'i config'i ezer.
202
+ * S3 credential'ları config'e yazılmaz — `JSKELET_S3_ACCESS_KEY_ID` /
203
+ * `JSKELET_S3_SECRET_ACCESS_KEY` (ve isteğe bağlı `JSKELET_S3_SESSION_TOKEN`).
204
+ * Bucket için `JSKELET_LOG_BUCKET` (veya `JSKELET_S3_BUCKET`) env'i config'i
205
+ * ezer ve `ayberkenis/jskelet/logs` gibi `bucket/prefix` yolunu kabul eder.
206
+ * Uyumlu API adresi `JSKELET_S3_API_URL` (R2 / MinIO).
205
207
  */
206
208
  export const DEFAULT_LOGS = {
207
209
  console: true,
@@ -402,6 +402,34 @@ function normalizeCloudflare(raw) {
402
402
 
403
403
  const LOG_KINDS = new Set(["http", "event", "error"]);
404
404
 
405
+ /**
406
+ * `bucket`, `JSKELET_LOG_BUCKET` veya `JSKELET_S3_BUCKET` değeri
407
+ * `ayberkenis/jskelet/logs` gibi bir yol olabilir: ilk segment bucket adı,
408
+ * kalanı nesne öneki. Böylece tek env ile hem kova hem klasör verilmiş olur.
409
+ *
410
+ * @param {string | null} value
411
+ * @returns {{ bucket: string | null, prefix: string | null }}
412
+ * `prefix` null → yol öneki taşımıyor; config/varsayılan kalsın.
413
+ */
414
+ export function splitS3BucketPath(value) {
415
+ if (!value) return { bucket: null, prefix: null };
416
+
417
+ const trimmed = value.replace(/^\/+|\/+$/g, "");
418
+ if (!trimmed) return { bucket: null, prefix: null };
419
+
420
+ const slash = trimmed.indexOf("/");
421
+ if (slash < 0) return { bucket: trimmed, prefix: null };
422
+
423
+ const bucket = trimmed.slice(0, slash);
424
+ const rest = trimmed.slice(slash + 1).replace(/^\/+|\/+$/g, "");
425
+ if (!bucket) return { bucket: null, prefix: null };
426
+
427
+ return {
428
+ bucket,
429
+ prefix: rest ? `${rest}/` : null,
430
+ };
431
+ }
432
+
405
433
  /**
406
434
  * Kalıcı log sink'leri. Bozuk bir `kinds` listesi siteyi düşürmemeli —
407
435
  * bilinmeyen girdiler atılır; hiç geçerli tür kalmazsa varsayılana dönülür.
@@ -436,8 +464,28 @@ export function normalizeLogs(raw) {
436
464
  /** @param {unknown} value */
437
465
  const text = (value) => (typeof value === "string" && value ? value : null);
438
466
 
439
- const envBucket = process.env.JSKELET_LOG_BUCKET;
440
- const envRegion = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
467
+ const bucketPath = splitS3BucketPath(
468
+ text(process.env.JSKELET_LOG_BUCKET) ??
469
+ text(process.env.JSKELET_S3_BUCKET) ??
470
+ text(s3Raw.bucket),
471
+ );
472
+
473
+ const configPrefix =
474
+ typeof s3Raw.prefix === "string" && s3Raw.prefix
475
+ ? s3Raw.prefix.endsWith("/")
476
+ ? s3Raw.prefix
477
+ : `${s3Raw.prefix}/`
478
+ : DEFAULT_LOGS.s3.prefix;
479
+
480
+ const endpoint =
481
+ text(process.env.JSKELET_S3_API_URL) ?? text(s3Raw.endpoint);
482
+
483
+ // R2 / uyumlu API'lerde bölge çoğu zaman `auto`; endpoint varken region
484
+ // zorunlu tutulursa tek satırlık env kurulumu boşa düşer.
485
+ const region =
486
+ text(s3Raw.region) ??
487
+ text(process.env.JSKELET_S3_REGION) ??
488
+ (endpoint ? "auto" : null);
441
489
 
442
490
  return {
443
491
  console: source.console !== false,
@@ -452,13 +500,11 @@ export function normalizeLogs(raw) {
452
500
  },
453
501
  s3: {
454
502
  enabled: s3Raw.enabled === true,
455
- bucket: text(envBucket) ?? text(s3Raw.bucket),
456
- prefix:
457
- typeof s3Raw.prefix === "string" && s3Raw.prefix
458
- ? s3Raw.prefix
459
- : DEFAULT_LOGS.s3.prefix,
460
- region: text(s3Raw.region) ?? text(envRegion),
461
- endpoint: text(s3Raw.endpoint),
503
+ bucket: bucketPath.bucket,
504
+ // Yoldaki önek tek env ile klasör vermeyi mümkün kılar; yoksa config.
505
+ prefix: bucketPath.prefix ?? configPrefix,
506
+ region,
507
+ endpoint,
462
508
  flushIntervalMs:
463
509
  Number.isFinite(flush) && flush >= 500
464
510
  ? Math.min(60_000, Math.floor(flush))
@@ -27,13 +27,13 @@ let accessMounted = false;
27
27
  * @returns {import('./s3-put.js').AwsCredentials | null}
28
28
  */
29
29
  function readCredentials() {
30
- const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
31
- const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
30
+ const accessKeyId = process.env.JSKELET_S3_ACCESS_KEY_ID;
31
+ const secretAccessKey = process.env.JSKELET_S3_SECRET_ACCESS_KEY;
32
32
  if (!accessKeyId || !secretAccessKey) return null;
33
33
  return {
34
34
  accessKeyId,
35
35
  secretAccessKey,
36
- sessionToken: process.env.AWS_SESSION_TOKEN || null,
36
+ sessionToken: process.env.JSKELET_S3_SESSION_TOKEN || null,
37
37
  };
38
38
  }
39
39
 
@@ -81,12 +81,16 @@ export async function configureLogs(config) {
81
81
  if (logs.s3.enabled) {
82
82
  const credentials = readCredentials();
83
83
  if (!logs.s3.bucket) {
84
- console.warn("[logs] s3.enabled but no bucket (set JSKELET_LOG_BUCKET or logs.s3.bucket)");
84
+ console.warn(
85
+ "[logs] s3.enabled but no bucket (set JSKELET_LOG_BUCKET or logs.s3.bucket)",
86
+ );
85
87
  } else if (!logs.s3.region) {
86
- console.warn("[logs] s3.enabled but no region (set AWS_REGION or logs.s3.region)");
88
+ console.warn(
89
+ "[logs] s3.enabled but no region (set JSKELET_S3_REGION or logs.s3.region)",
90
+ );
87
91
  } else if (!credentials) {
88
92
  console.warn(
89
- "[logs] s3.enabled but AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY missing — S3 sink disabled",
93
+ "[logs] s3.enabled but JSKELET_S3_ACCESS_KEY_ID / JSKELET_S3_SECRET_ACCESS_KEY missing — S3 sink disabled",
90
94
  );
91
95
  } else {
92
96
  next.push(
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * `@aws-sdk/client-s3` taşınmıyor: tek operasyon için SDK'nın maliyeti ve
5
5
  * peer yüzeyi gereksiz. Credential zinciri env'den okunur
6
- * (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`);
7
- * IAM role / instance metadata yok — container'a key vermeyen kurulumlar
8
- * için sink zaten açılışta no-op'a düşer.
6
+ * (`JSKELET_S3_ACCESS_KEY_ID` / `JSKELET_S3_SECRET_ACCESS_KEY` /
7
+ * `JSKELET_S3_SESSION_TOKEN`); IAM role / instance metadata yok —
8
+ * container'a key vermeyen kurulumlar için sink zaten açılışta no-op'a düşer.
9
9
  */
10
10
  import crypto from "node:crypto";
11
11