jskelet 0.6.9 → 0.6.11

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
@@ -10,16 +10,18 @@ one is listed under a **Breaking** heading.
10
10
 
11
11
  ### Added
12
12
 
13
- - Optional CDN asset publishing: `build.assets.prefix` /
14
- `JSKELET_ASSET_PREFIX` prefixes `asset()` and optimised image URLs (manifest
15
- stays relative on disk). When `JSKELET_ASSETS_BUCKET` (or
16
- `build.assets.upload`) is set, production `jskelet build` PutObjects
17
- `public/assets/` (and optional `include` dirs) to S3 after precompress.
18
- Credentials prefer standard `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` /
19
- `AWS_SESSION_TOKEN`, then `JSKELET_S3_*`. The Assets upload step always
20
- appears on production builds; when off it logs a one-line reason
21
- (`skipped (no JSKELET_ASSETS_BUCKET)`, `skipped (credentials missing)`, …).
22
- Watch mode never uploads. `.br` / `.gz` siblings are skipped.
13
+ - Optional CDN asset publishing via `build.assets`: `prefix` rewrites
14
+ `asset()` / optimised image URLs (manifest stays relative on disk);
15
+ `upload.bucket` / `keyPrefix` / `region` / `endpoint` come from config only.
16
+ Production `jskelet build` PutObjects `public/assets/` (and optional
17
+ `include` dirs) after precompress. Only credentials are read from env
18
+ (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, then
19
+ `JSKELET_S3_*`, then ECS task-role / EC2 instance-profile metadata). The
20
+ Assets upload step always appears on production builds; when off it logs a
21
+ one-line reason (`skipped (no build.assets.upload.bucket)`,
22
+ `skipped (credentials missing)`, …). A URL in `upload.bucket` is rejected
23
+ with a warning (CDN origin belongs in `prefix`). Watch mode never uploads.
24
+ `.br` / `.gz` siblings are skipped.
23
25
 
24
26
  ### Changed
25
27
 
@@ -96,10 +96,11 @@ export default {
96
96
  groups: { charts: ["chart.js"] },
97
97
  },
98
98
  assets: {
99
- prefix: process.env.JSKELET_ASSET_PREFIX,
99
+ prefix: "https://static.ornek.com/js",
100
100
  upload: {
101
- enabled: Boolean(process.env.JSKELET_ASSETS_BUCKET),
102
- bucket: process.env.JSKELET_ASSETS_BUCKET,
101
+ bucket: "static.ornek.com", // S3 bucket adı (noktalı olabilir)
102
+ keyPrefix: "js",
103
+ region: "eu-central-1",
103
104
  },
104
105
  },
105
106
  },
@@ -628,21 +629,22 @@ değişmez** — asset'ler origin'den servis edilir.
628
629
 
629
630
  | Alan | Tip | Varsayılan | Anlamı |
630
631
  | --- | --- | --- | --- |
631
- | `assets.prefix` | `string` | `""` | `asset()` / optimize görsel URL öneki (sonunda `/` yok). `JSKELET_ASSET_PREFIX` ezer. Manifest diskte göreli kalır. |
632
- | `assets.upload.enabled` | `boolean` | `false` | `true` veya `JSKELET_ASSETS_BUCKET` doluysa açılır; açık `false` ezer. |
633
- | `assets.upload.bucket` | `string \| null` | `null` | Bucket veya `bucket/key/prefix`. `JSKELET_ASSETS_BUCKET` ezer. |
634
- | `assets.upload.keyPrefix` | `string` | `""` | Yolda verilmediyse nesne anahtarı öneki (`prefix` takma adı da kabul). |
635
- | `assets.upload.region` | `string` | `us-east-1` | `AWS_REGION` / `AWS_DEFAULT_REGION` / `JSKELET_S3_REGION`; custom endpoint'te varsayılan `auto`. |
636
- | `assets.upload.endpoint` | `string \| null` | `null` | S3-uyumlu API; `AWS_ENDPOINT_URL` / `JSKELET_S3_API_URL` ezer. |
632
+ | `assets.prefix` | `string` | `""` | CDN origin; `asset()` URL öneki (sonunda `/` yok). Yalnızca config. Manifest diskte göreli kalır. |
633
+ | `assets.upload.enabled` | `boolean` | `false` | `true` veya config'te `bucket` doluysa açılır; açık `false` ezer. |
634
+ | `assets.upload.bucket` | `string \| null` | `null` | **S3 bucket adı** (`static.ornek.com` gibi noktalı adlar geçerli) veya `bucket/key/prefix`. `https://…` şeması kabul edilmez — o `prefix`. |
635
+ | `assets.upload.keyPrefix` | `string` | `""` | Nesne anahtarı öneki; bucket yolunda verilmişse o kazanır (`prefix` takma adı da kabul). |
636
+ | `assets.upload.region` | `string` | `us-east-1` | Config; custom `endpoint` varken varsayılan `auto`. |
637
+ | `assets.upload.endpoint` | `string \| null` | `null` | S3-uyumlu API (R2 / MinIO); config. |
637
638
  | `assets.upload.include` | `string[]` | `["assets"]` | `public/` altı dizinler (`assets`, isteğe `fonts`, …). |
638
639
  | `assets.upload.cacheControl` | `string` | `public, max-age=31536000, immutable` | PutObject `Cache-Control`. |
639
640
 
640
- Credential config'e yazılmaz. Öncelik: `AWS_ACCESS_KEY_ID` /
641
- `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, yoksa `JSKELET_S3_*`. Production
642
- build'de Assets upload adımı her zaman görünür; kapalıysa
643
- `skipped (no JSKELET_ASSETS_BUCKET)` / `skipped (credentials missing)` gibi tek
644
- satır sebep basılır (build düşmez). Watch turunda adım yok. `.br` / `.gz`
645
- yüklenmez. Ayrıntı: [08-build.md](./08-build.md), [10-dagitim.md](./10-dagitim.md).
641
+ **Credential config'e yazılmaz.** Zincir: `AWS_ACCESS_KEY_ID` /
642
+ `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` (yoksa `JSKELET_S3_*`) → ECS/Fargate
643
+ task role (`AWS_CONTAINER_CREDENTIALS_*`) → EC2 instance profile (IMDSv2).
644
+ Prefix, bucket, region vb. config'te yazılır. Task role varken env key şart
645
+ değil. Production build'de Assets upload adımı her zaman görünür; kapalıysa
646
+ `skipped (…)` ile sebep basılır. Watch'ta adım yok. `.br` / `.gz` yüklenmez.
647
+ Ayrıntı: [08-build.md](./08-build.md), [10-dagitim.md](./10-dagitim.md).
646
648
 
647
649
  ```js
648
650
  build: {
@@ -657,10 +659,12 @@ build: {
657
659
  },
658
660
  },
659
661
  assets: {
660
- prefix: process.env.JSKELET_ASSET_PREFIX, // https://dxxxx.cloudfront.net
662
+ // HTML: /assets/app.css → https://static.ornek.com/js/assets/app.css
663
+ prefix: "https://static.ornek.com/js",
661
664
  upload: {
662
- enabled: Boolean(process.env.JSKELET_ASSETS_BUCKET),
663
- bucket: process.env.JSKELET_ASSETS_BUCKET, // my-bucket veya my-bucket/static
665
+ bucket: "static.ornek.com", // veya "static.ornek.com/js" → keyPrefix js/
666
+ keyPrefix: "js", // S3 key: js/assets/…
667
+ region: "eu-central-1",
664
668
  include: ["assets", "fonts"],
665
669
  },
666
670
  },
@@ -1228,13 +1232,9 @@ basılmaz.
1228
1232
  | `DEV_GATE` | `devGate` | kapalı | `1` gate'i açar, `0` config'te açık olsa da kapatır. `DEV_TOKEN` tek başına açmaz. [09](./09-dev-araclari.md) |
1229
1233
  | `DEV_TOKEN` | `devGate`, `prewarm` | — | Gate açıkken beklenen sır. Yoksa veya gate kapalıysa site açık kalır. Isıtma, gate açıkken token'ı çerez olarak taşır. [09](./09-dev-araclari.md) |
1230
1234
  | `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) |
1231
- | `JSKELET_ASSET_PREFIX` | `build.assets.prefix` | — | CDN origin; `asset()` URL'lerine eklenir (sonunda `/` yok) |
1232
- | `JSKELET_ASSETS_BUCKET` | `build.assets.upload` | — | Asset upload hedefi: bucket veya `bucket/prefix`. Credential ile birlikte production build sonunda PutObject |
1233
1235
  | `AWS_ACCESS_KEY_ID` | S3 PutObject | — | Standart AWS imza anahtarı (log + asset upload); `JSKELET_S3_*`'dan önce |
1234
1236
  | `AWS_SECRET_ACCESS_KEY` | S3 PutObject | — | Standart AWS imza sırrı |
1235
1237
  | `AWS_SESSION_TOKEN` | S3 PutObject | — | Geçici credential (OIDC / STS) |
1236
- | `AWS_REGION` / `AWS_DEFAULT_REGION` | `build.assets.upload` | — | Asset upload bölgesi |
1237
- | `AWS_ENDPOINT_URL` | `build.assets.upload` | — | S3-uyumlu endpoint (asset upload) |
1238
1238
  | `JSKELET_LOG_BUCKET` | `logs.s3` | — | Log hedefi: bucket ya da `bucket/prefix` yolu. Credential ile birlikte varsa sink otomatik açılır |
1239
1239
  | `JSKELET_S3_BUCKET` | `logs.s3` | — | `JSKELET_LOG_BUCKET` yoksa bucket; `JSKELET_S3_KEY_PREFIX` ile birleşir |
1240
1240
  | `JSKELET_S3_KEY_PREFIX` | `logs.s3` | — | `JSKELET_S3_BUCKET` ile kullanılır (`bucket/prefix`) |
package/docs/08-build.md CHANGED
@@ -35,7 +35,7 @@ Sıra rastgele değil:
35
35
  adımı yalnızca ham dosyaları kopyalar.
36
36
  - **Görseller watch turunda hiç çalışmaz:** `sharp` ile yeniden kodlama pahalı.
37
37
  - **Assets upload watch'ta yok:** yalnızca production `jskelet build`; kapalıysa
38
- yine satır basılır (`skipped (no JSKELET_ASSETS_BUCKET)` vb.).
38
+ yine satır basılır (`skipped (no build.assets.upload.bucket)` vb.).
39
39
 
40
40
  Görevler yalnızca ilgili yapılandırma varsa çalışır. Font tanımlamayan bir proje
41
41
  font adımını hiç görmez; bu, "framework her projeye kendi varsayımlarını
@@ -78,24 +78,24 @@ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
78
78
  ```
79
79
 
80
80
  - `asset(name)` manifest'te varsa hash'li URL'i, yoksa `/assets/<name>` döner.
81
- `build.assets.prefix` / `JSKELET_ASSET_PREFIX` doluysa bu göreli yolun önüne
82
- CDN origin eklenir (manifest diskte göreli kalır).
81
+ `build.assets.prefix` doluysa bu göreli yolun önüne CDN origin eklenir
82
+ (manifest diskte göreli kalır).
83
83
  - `hasAsset(name)` manifest'te olup olmadığını söyler.
84
84
 
85
85
  ### CDN prefix ve S3 upload
86
86
 
87
- Varsayılan kapalıdır. Açılınca:
87
+ Varsayılan kapalıdır. Prefix / bucket / keyPrefix config'te; credential
88
+ env → ECS task role → EC2 IMDS zincirinden. Açılınca:
88
89
 
89
90
  1. Production build `public/assets/` (ve `include` listesindeki diğer dizinler)
90
91
  altındaki dosyaları S3'e PutObject ile kopyalar.
91
- 2. Çalışma anında `asset()` / optimize görseller `JSKELET_ASSET_PREFIX` ile
92
+ 2. Çalışma anında `asset()` / optimize görseller `build.assets.prefix` ile
92
93
  absolute CDN URL basar. Entry CDN'deyse esbuild chunk'ları göreli yolla aynı
93
94
  host'a gider.
94
95
 
95
- Credential: `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (önce), yoksa
96
- `JSKELET_S3_*`. Bucket: `JSKELET_ASSETS_BUCKET`. Config sözlüğü:
97
- [07-yapilandirma.md](./07-yapilandirma.md#build). Dağıtım notları:
98
- [10-dagitim.md](./10-dagitim.md).
96
+ Bucket adı noktalı olabilir (`static.ornek.com`); `https://…` şeması
97
+ `prefix`'e yazılır. Config sözlüğü: [07-yapilandirma.md](./07-yapilandirma.md#build).
98
+ Dağıtım notları: [10-dagitim.md](./10-dagitim.md).
99
99
 
100
100
  Build çalışmadıysa uygulama yine ayağa kalkar: `hasAsset()` false olur, layout
101
101
  stylesheet ve script etiketlerini hiç basmaz. `jskelet build` unutulduğunda hata
@@ -51,20 +51,30 @@ ayarlamayı düşünmeniz gerekenler:
51
51
  | `PREWARM_MAX` | Site boyutuna göre | Açılışta ısıtılacak sayfa sayısı |
52
52
  | `PREWARM_INTERVAL_SECONDS` | `0` ya da uzun bir değer | Hiç ziyaret edilmeyen sayfaları sıcak tutmak isterseniz |
53
53
  | `DEV_GATE` + `DEV_TOKEN` | Yalnızca staging'de | Yayına açılmamış ortamı gizler. Token tek başına siteyi kilitlemez |
54
- | `JSKELET_S3_*` / `AWS_*` | Access log veya asset'leri S3'e yazıyorsanız | Credential; AWS deploy'da `AWS_ACCESS_KEY_ID` vb. yeter. Ayrıntı [07](./07-yapilandirma.md) |
55
- | `JSKELET_ASSETS_BUCKET` | Hash'li asset'leri S3/CloudFront'a basıyorsanız | Production `jskelet build` sonunda PutObject; `JSKELET_ASSET_PREFIX` ile HTML CDN URL basar |
54
+ | `JSKELET_S3_*` / `AWS_*` | Access log veya asset'leri S3'e yazıyorsanız | Yalnızca credential; bucket/prefix `jskelet.config.mjs` → `build.assets`. Ayrıntı [07](./07-yapilandirma.md) |
56
55
 
57
56
  ### Asset'leri S3 / CloudFront'a basmak
58
57
 
59
- Varsayılan kapalıdır. CI veya prod build ortamında:
58
+ Varsayılan kapalıdır. Config'te prefix + bucket; CI'da yalnızca credential:
59
+
60
+ ```js
61
+ // jskelet.config.mjs
62
+ build: {
63
+ assets: {
64
+ prefix: "https://static.ornek.com/js",
65
+ upload: {
66
+ bucket: "static.ornek.com", // veya "static.ornek.com/js"
67
+ keyPrefix: "js",
68
+ region: "eu-central-1",
69
+ },
70
+ },
71
+ },
72
+ ```
60
73
 
61
74
  ```bash
62
- JSKELET_ASSETS_BUCKET=my-app-assets # veya my-app-assets/static
63
- JSKELET_ASSET_PREFIX=https://dxxxx.cloudfront.net
64
- AWS_REGION=eu-central-1
75
+ # CI / local: explicit keys. ECS/EC2'de task/instance role yeter — key şart değil.
65
76
  AWS_ACCESS_KEY_ID=…
66
77
  AWS_SECRET_ACCESS_KEY=…
67
- # geçici credential ise:
68
78
  # AWS_SESSION_TOKEN=…
69
79
  ```
70
80
 
@@ -100,10 +100,11 @@ export default {
100
100
  groups: { charts: ["chart.js"] },
101
101
  },
102
102
  assets: {
103
- prefix: process.env.JSKELET_ASSET_PREFIX,
103
+ prefix: "https://static.example.com/js",
104
104
  upload: {
105
- enabled: Boolean(process.env.JSKELET_ASSETS_BUCKET),
106
- bucket: process.env.JSKELET_ASSETS_BUCKET,
105
+ bucket: "static.example.com", // S3 bucket name (dots are fine)
106
+ keyPrefix: "js",
107
+ region: "eu-central-1",
107
108
  },
108
109
  },
109
110
  },
@@ -641,22 +642,22 @@ changes** — assets stay on the origin.
641
642
 
642
643
  | Field | Type | Default | Meaning |
643
644
  | --- | --- | --- | --- |
644
- | `assets.prefix` | `string` | `""` | Prefix for `asset()` / optimised image URLs (no trailing `/`). `JSKELET_ASSET_PREFIX` overrides. The on-disk manifest stays relative. |
645
- | `assets.upload.enabled` | `boolean` | `false` | Turns on when `true` or when `JSKELET_ASSETS_BUCKET` is set; explicit `false` wins. |
646
- | `assets.upload.bucket` | `string \| null` | `null` | Bucket or `bucket/key/prefix`. `JSKELET_ASSETS_BUCKET` overrides. |
647
- | `assets.upload.keyPrefix` | `string` | `""` | Object key prefix when not given in the path (`prefix` is accepted as an alias). |
648
- | `assets.upload.region` | `string` | `us-east-1` | `AWS_REGION` / `AWS_DEFAULT_REGION` / `JSKELET_S3_REGION`; defaults to `auto` with a custom endpoint. |
649
- | `assets.upload.endpoint` | `string \| null` | `null` | S3-compatible API; `AWS_ENDPOINT_URL` / `JSKELET_S3_API_URL` overrides. |
645
+ | `assets.prefix` | `string` | `""` | CDN origin; prefix for `asset()` URLs (no trailing `/`). Config only. The on-disk manifest stays relative. |
646
+ | `assets.upload.enabled` | `boolean` | `false` | Turns on when `true` or when `bucket` is set in config; explicit `false` wins. |
647
+ | `assets.upload.bucket` | `string \| null` | `null` | **S3 bucket name** (dotted names like `static.example.com` are fine) or `bucket/key/prefix`. A `https://…` scheme is rejected — that belongs in `prefix`. |
648
+ | `assets.upload.keyPrefix` | `string` | `""` | Object key prefix; a path on the bucket wins (`prefix` is accepted as an alias). |
649
+ | `assets.upload.region` | `string` | `us-east-1` | Config; defaults to `auto` when `endpoint` is set. |
650
+ | `assets.upload.endpoint` | `string \| null` | `null` | S3-compatible API (R2 / MinIO); config. |
650
651
  | `assets.upload.include` | `string[]` | `["assets"]` | Directories under `public/` (`assets`, optionally `fonts`, …). |
651
652
  | `assets.upload.cacheControl` | `string` | `public, max-age=31536000, immutable` | PutObject `Cache-Control`. |
652
653
 
653
- Credentials are not written in the config. Preference order:
654
- `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, then
655
- `JSKELET_S3_*`. On a production build the Assets upload step always appears;
656
- when off it logs a one-line reason such as
657
- `skipped (no JSKELET_ASSETS_BUCKET)` or `skipped (credentials missing)` (the
658
- build does not fail). No upload on watch passes. `.br` / `.gz` siblings are not
659
- uploaded. Details: [08-build.md](./08-build.md),
654
+ **Credentials are not written in the config.** Chain: `AWS_ACCESS_KEY_ID` /
655
+ `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` (else `JSKELET_S3_*`) → ECS/Fargate
656
+ task role (`AWS_CONTAINER_CREDENTIALS_*`) → EC2 instance profile (IMDSv2).
657
+ Prefix, bucket, region, etc. live in the config. With a task role, env keys are
658
+ not required. On a production build the Assets upload step always appears; when
659
+ off it logs `skipped (…)`. No upload on watch passes. `.br` / `.gz` siblings are
660
+ not uploaded. Details: [08-build.md](./08-build.md),
660
661
  [10-deployment.md](./10-deployment.md).
661
662
 
662
663
  ```js
@@ -672,10 +673,12 @@ build: {
672
673
  },
673
674
  },
674
675
  assets: {
675
- prefix: process.env.JSKELET_ASSET_PREFIX, // https://dxxxx.cloudfront.net
676
+ // HTML: /assets/app.css → https://static.example.com/js/assets/app.css
677
+ prefix: "https://static.example.com/js",
676
678
  upload: {
677
- enabled: Boolean(process.env.JSKELET_ASSETS_BUCKET),
678
- bucket: process.env.JSKELET_ASSETS_BUCKET, // my-bucket or my-bucket/static
679
+ bucket: "static.example.com", // or "static.example.com/js" → keyPrefix js/
680
+ keyPrefix: "js", // S3 key: js/assets/…
681
+ region: "eu-central-1",
679
682
  include: ["assets", "fonts"],
680
683
  },
681
684
  },
@@ -1252,13 +1255,9 @@ and no warning is printed.
1252
1255
  | `DEV_GATE` | `devGate` | off | `1` turns the gate on, `0` turns it off even when config enabled it. `DEV_TOKEN` alone does not turn it on. [09](./09-dev-tools.md) |
1253
1256
  | `DEV_TOKEN` | `devGate`, `prewarm` | — | The secret expected while the gate is on. If it is missing, or the gate is off, the site stays public. Prewarming carries the token as a cookie only while the gate is on. [09](./09-dev-tools.md) |
1254
1257
  | `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) |
1255
- | `JSKELET_ASSET_PREFIX` | `build.assets.prefix` | — | CDN origin prepended to `asset()` URLs (no trailing `/`) |
1256
- | `JSKELET_ASSETS_BUCKET` | `build.assets.upload` | — | Asset upload target: bucket or `bucket/prefix`. With credentials, production build PutObject |
1257
1258
  | `AWS_ACCESS_KEY_ID` | S3 PutObject | — | Standard AWS signing key (logs + asset upload); before `JSKELET_S3_*` |
1258
1259
  | `AWS_SECRET_ACCESS_KEY` | S3 PutObject | — | Standard AWS signing secret |
1259
1260
  | `AWS_SESSION_TOKEN` | S3 PutObject | — | Temporary credentials (OIDC / STS) |
1260
- | `AWS_REGION` / `AWS_DEFAULT_REGION` | `build.assets.upload` | — | Asset upload region |
1261
- | `AWS_ENDPOINT_URL` | `build.assets.upload` | — | S3-compatible endpoint (asset upload) |
1262
1261
  | `JSKELET_LOG_BUCKET` | `logs.s3` | — | Log target: bucket or `bucket/prefix` path. With credentials, the sink turns on automatically |
1263
1262
  | `JSKELET_S3_BUCKET` | `logs.s3` | — | Bucket when `JSKELET_LOG_BUCKET` is unset; joins with `JSKELET_S3_KEY_PREFIX` |
1264
1263
  | `JSKELET_S3_KEY_PREFIX` | `logs.s3` | — | Used with `JSKELET_S3_BUCKET` (`bucket/prefix`) |
@@ -35,7 +35,7 @@ The order is not arbitrary:
35
35
  the S3 step copies only the raw files.
36
36
  - **Images never run on a watch pass:** re-encoding with `sharp` is expensive.
37
37
  - **Assets upload never runs on watch:** only production `jskelet build`; when
38
- off the step still prints (`skipped (no JSKELET_ASSETS_BUCKET)`, etc.).
38
+ off the step still prints (`skipped (no build.assets.upload.bucket)`, etc.).
39
39
 
40
40
  Tasks only run if the relevant configuration exists. A project that does not
41
41
  define any fonts never sees the font step; this is the build-side counterpart of
@@ -81,25 +81,24 @@ They are passed to templates automatically; in server code,
81
81
  ```
82
82
 
83
83
  - `asset(name)` returns the hashed URL if it is in the manifest, otherwise
84
- `/assets/<name>`. When `build.assets.prefix` / `JSKELET_ASSET_PREFIX` is set,
85
- that relative path is prefixed with the CDN origin (the on-disk manifest stays
86
- relative).
84
+ `/assets/<name>`. When `build.assets.prefix` is set, that relative path is
85
+ prefixed with the CDN origin (the on-disk manifest stays relative).
87
86
  - `hasAsset(name)` tells you whether it is in the manifest.
88
87
 
89
88
  ### CDN prefix and S3 upload
90
89
 
91
- Off by default. When enabled:
90
+ Off by default. Prefix / bucket / keyPrefix come from config; credentials
91
+ resolve from env → ECS task role → EC2 IMDS. When enabled:
92
92
 
93
93
  1. A production build PutObjects files under `public/assets/` (and any other
94
94
  directories in `include`) to S3.
95
95
  2. At runtime `asset()` / optimised images emit absolute CDN URLs via
96
- `JSKELET_ASSET_PREFIX`. When the entry script is on the CDN, esbuild chunks
96
+ `build.assets.prefix`. When the entry script is on the CDN, esbuild chunks
97
97
  resolve relatively on the same host.
98
98
 
99
- Credentials: `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` first, else
100
- `JSKELET_S3_*`. Bucket: `JSKELET_ASSETS_BUCKET`. Config reference:
101
- [07-configuration.md](./07-configuration.md#build). Deploy notes:
102
- [10-deployment.md](./10-deployment.md).
99
+ Bucket names may include dots (`static.example.com`); put `https://…` in
100
+ `prefix`. Config reference: [07-configuration.md](./07-configuration.md#build).
101
+ Deploy notes: [10-deployment.md](./10-deployment.md).
103
102
 
104
103
  If the build has not run, the application still comes up: `hasAsset()` is false
105
104
  and the layout never emits the stylesheet and script tags. When `jskelet build`
@@ -52,20 +52,30 @@ considering in production:
52
52
  | `PREWARM_MAX` | Depends on site size | Number of pages warmed at startup |
53
53
  | `PREWARM_INTERVAL_SECONDS` | `0` or a long value | If you want to keep never-visited pages warm |
54
54
  | `DEV_GATE` + `DEV_TOKEN` | Staging only | Hides an environment that is not public yet. The token alone does not lock the site |
55
- | `JSKELET_S3_*` / `AWS_*` | If you write access logs or assets to S3 | Credentials; on AWS, `AWS_ACCESS_KEY_ID` etc. are enough. Details in [07](./07-configuration.md) |
56
- | `JSKELET_ASSETS_BUCKET` | If you publish hashed assets to S3/CloudFront | PutObject at the end of production `jskelet build`; `JSKELET_ASSET_PREFIX` makes HTML emit CDN URLs |
55
+ | `JSKELET_S3_*` / `AWS_*` | If you write access logs or assets to S3 | Credentials only; bucket/prefix live in `jskelet.config.mjs` → `build.assets`. Details in [07](./07-configuration.md) |
57
56
 
58
57
  ### Publishing assets to S3 / CloudFront
59
58
 
60
- Off by default. In CI or a production build environment:
59
+ Off by default. Prefix + bucket in config; credentials only in CI:
60
+
61
+ ```js
62
+ // jskelet.config.mjs
63
+ build: {
64
+ assets: {
65
+ prefix: "https://static.example.com/js",
66
+ upload: {
67
+ bucket: "static.example.com", // or "static.example.com/js"
68
+ keyPrefix: "js",
69
+ region: "eu-central-1",
70
+ },
71
+ },
72
+ },
73
+ ```
61
74
 
62
75
  ```bash
63
- JSKELET_ASSETS_BUCKET=my-app-assets # or my-app-assets/static
64
- JSKELET_ASSET_PREFIX=https://dxxxx.cloudfront.net
65
- AWS_REGION=eu-central-1
76
+ # CI / local: explicit keys. On ECS/EC2 a task/instance role is enough — no keys required.
66
77
  AWS_ACCESS_KEY_ID=…
67
78
  AWS_SECRET_ACCESS_KEY=…
68
- # for temporary credentials:
69
79
  # AWS_SESSION_TOKEN=…
70
80
  ```
71
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "A framework that feels like no framework: Express 5 + build-time .jsk SSR (optional EJS peer), vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * Production build sonunda hash'li varlıkları S3'e PutObject ile kopyalar.
3
3
  *
4
- * Watch turunda çalışmaz (çağıran sorumlu). Credential env'den gelir
5
- * (`AWS_*` önce, `JSKELET_S3_*` yedek). Bucket / credential yoksa veya upload
6
- * kapalıysa adım yine görünür: `skipped (…)` ile sebep basılır, build düşmez.
4
+ * Watch turunda çalışmaz (çağıran sorumlu). Credential: env (`AWS_*` /
5
+ * `JSKELET_S3_*`) → ECS task role → EC2 instance profile. Bucket yoksa veya
6
+ * upload kapalıysa adım yine görünür: `skipped (…)` ile sebep basılır.
7
+ * Credential yalnızca env+metadata zinciri boşsa skip; task role varken
8
+ * env key şart değil.
7
9
  *
8
10
  * `.br` / `.gz` kardeşleri yüklenmez: edge kendi sıkıştırmasını yapsın;
9
11
  * origin precompress Express için kalır.
10
12
  */
11
13
  import fs from "node:fs";
12
14
  import path from "node:path";
13
- import { readS3CredentialsFromEnv } from "../../config/index.js";
15
+ import { resolveS3Credentials } from "../../server/logs/s3-credentials.js";
14
16
  import { putObject } from "../../server/logs/s3-put.js";
15
17
  import * as log from "../../log.mjs";
16
18
  import { paths } from "../paths.mjs";
@@ -88,7 +90,7 @@ function resolveIncludeDir(name) {
88
90
  }
89
91
 
90
92
  /**
91
- * Upload yapılmayacaksa kısa sebep; yapılacaksa `null`.
93
+ * Config kapısı — credential burada bakılmaz (task role async).
92
94
  *
93
95
  * @param {import('../../config/index.js').ResolvedConfig} config
94
96
  * @returns {string | null}
@@ -97,22 +99,21 @@ export function assetsUploadSkipReason(config) {
97
99
  const upload = config.build?.assets?.upload;
98
100
 
99
101
  if (!upload?.enabled) {
100
- // Bucket yoksa asıl eksik env'i söyle — `enabled: false` çoğu zaman
102
+ // Bucket yoksa asıl eksik alanı söyle — `enabled: false` çoğu zaman
101
103
  // bucket tanımlanmadığı için dolaylıdır.
102
- if (!upload?.bucket) return "no JSKELET_ASSETS_BUCKET";
104
+ if (!upload?.bucket) return "no build.assets.upload.bucket";
103
105
  return "build.assets.upload.enabled is false";
104
106
  }
105
107
 
106
- if (!upload.bucket) return "no JSKELET_ASSETS_BUCKET";
107
-
108
- if (!readS3CredentialsFromEnv()) return "credentials missing";
108
+ if (!upload.bucket) return "no build.assets.upload.bucket";
109
109
 
110
110
  return null;
111
111
  }
112
112
 
113
113
  /**
114
114
  * @param {import('../../config/index.js').ResolvedConfig} config
115
- * @param {{ put?: typeof putObject }} [options]
115
+ * @param {{ put?: typeof putObject,
116
+ * resolveCredentials?: typeof resolveS3Credentials }} [options]
116
117
  * @returns {Promise<void>}
117
118
  */
118
119
  export async function uploadAssets(config, options = {}) {
@@ -123,15 +124,17 @@ export async function uploadAssets(config, options = {}) {
123
124
  }
124
125
 
125
126
  const upload = config.build.assets.upload;
126
- const credentials = readS3CredentialsFromEnv();
127
- // skipReason null iken credential ve bucket vardır; TS/akış için daralt.
127
+ const resolve = options.resolveCredentials ?? resolveS3Credentials;
128
+ const credentials = await resolve();
128
129
  if (!upload.bucket || !credentials) {
129
130
  log.detail("skipped (credentials missing)");
130
131
  return;
131
132
  }
132
133
 
133
134
  const put = options.put ?? putObject;
134
- const keyPrefix = (upload.keyPrefix || "").replace(/^\/+/, "");
135
+ // Normalize bazen atlanır (test / elle config); önek `/` ile bitsin.
136
+ let keyPrefix = (upload.keyPrefix || "").replace(/^\/+/, "");
137
+ if (keyPrefix && !keyPrefix.endsWith("/")) keyPrefix += "/";
135
138
 
136
139
  /** @type {{ file: string, key: string }[]} */
137
140
  const jobs = [];
@@ -498,10 +498,11 @@ export const DEFAULT_BUILD = {
498
498
  chunkGroup: null,
499
499
  },
500
500
  assets: {
501
- /** CDN / CloudFront origin; sonunda `/` yok. Env: `JSKELET_ASSET_PREFIX`. */
501
+ /** CDN / CloudFront origin; sonunda `/` yok. Yalnızca config. */
502
502
  prefix: "",
503
503
  upload: {
504
504
  enabled: false,
505
+ /** S3 bucket adı (CDN URL değil). Yalnızca config; credential env'den. */
505
506
  bucket: null,
506
507
  /** Nesne anahtarı öneki (`bucket/path` split'inden veya config). */
507
508
  keyPrefix: "",
@@ -33,6 +33,10 @@ import path from "node:path";
33
33
  import process from "node:process";
34
34
  import { pathToFileURL } from "node:url";
35
35
  import { compilePattern, matchPattern } from "./pattern.js";
36
+ import {
37
+ readS3CredentialsFromEnv,
38
+ resolveS3Credentials,
39
+ } from "../server/logs/s3-credentials.js";
36
40
  import {
37
41
  DEFAULT_ADMIN,
38
42
  DEFAULT_AUTH,
@@ -64,6 +68,8 @@ import {
64
68
  DEFAULT_UPSTREAM_LIMIT,
65
69
  } from "./defaults.js";
66
70
 
71
+ export { readS3CredentialsFromEnv, resolveS3Credentials };
72
+
67
73
  /** Framework paketinin kökü — kendi şablonlarına ve varlıklarına erişir. */
68
74
  export const FRAMEWORK_ROOT = path.resolve(import.meta.dirname, "..", "..");
69
75
 
@@ -180,7 +186,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
180
186
  * @property {string} cacheControl
181
187
  *
182
188
  * @typedef {object} BuildAssetsConfig
183
- * @property {string} prefix CDN origin (`JSKELET_ASSET_PREFIX`); sonunda `/` yok.
189
+ * @property {string} prefix CDN origin (`build.assets.prefix`); sonunda `/` yok.
184
190
  * @property {BuildAssetsUploadConfig} upload
185
191
  *
186
192
  * @typedef {object} BuildConfig
@@ -527,32 +533,6 @@ function envText(value) {
527
533
  return typeof value === "string" && value ? value : null;
528
534
  }
529
535
 
530
- /**
531
- * S3 imza credential'ları. AWS deploy'da standart env önce gelir; R2/MinIO
532
- * için `JSKELET_S3_*` yedek. IAM instance/task role (IMDS) desteklenmez —
533
- * CI'da `configure-aws-credentials` gibi araçlar zaten `AWS_*` basar.
534
- *
535
- * @returns {{ accessKeyId: string, secretAccessKey: string,
536
- * sessionToken: string | null } | null}
537
- */
538
- export function readS3CredentialsFromEnv() {
539
- const accessKeyId =
540
- envText(process.env.AWS_ACCESS_KEY_ID) ??
541
- envText(process.env.JSKELET_S3_ACCESS_KEY_ID);
542
- const secretAccessKey =
543
- envText(process.env.AWS_SECRET_ACCESS_KEY) ??
544
- envText(process.env.JSKELET_S3_SECRET_ACCESS_KEY) ??
545
- envText(process.env.JSKELET_S3_ACCESS_SECRET);
546
- if (!accessKeyId || !secretAccessKey) return null;
547
- return {
548
- accessKeyId,
549
- secretAccessKey,
550
- sessionToken:
551
- envText(process.env.AWS_SESSION_TOKEN) ??
552
- envText(process.env.JSKELET_S3_SESSION_TOKEN),
553
- };
554
- }
555
-
556
536
  /**
557
537
  * Log hedefi. Öncelik: `JSKELET_LOG_BUCKET` → `JSKELET_S3_BUCKET`
558
538
  * (+ isteğe bağlı `JSKELET_S3_KEY_PREFIX`).
@@ -1186,7 +1166,34 @@ function defaultBuildClient() {
1186
1166
  }
1187
1167
 
1188
1168
  /**
1189
- * CDN prefix + S3 upload (`build.assets`). Credential config'e yazılmaz.
1169
+ * Asset upload bucket'ı. CDN URL buraya yazılmaz — o `build.assets.prefix`.
1170
+ *
1171
+ * @param {unknown} value
1172
+ * @returns {{ bucket: string | null, prefix: string | null }}
1173
+ */
1174
+ function normalizeAssetsBucket(value) {
1175
+ const text = typeof value === "string" ? value.trim() : "";
1176
+ if (!text) return { bucket: null, prefix: null };
1177
+
1178
+ // Noktalı bucket adları geçerli (`static.investvio.com`). Reddedilen yalnızca
1179
+ // şemalı URL — CDN origin `build.assets.prefix`'te kalır.
1180
+ if (/^https?:\/\//i.test(text)) {
1181
+ console.warn(
1182
+ "[config] build.assets.upload.bucket looks like a URL — use the S3 " +
1183
+ "bucket name without a scheme (e.g. \"static.investvio.com\" or " +
1184
+ "\"static.investvio.com/js\"); put https://… in build.assets.prefix",
1185
+ );
1186
+ return { bucket: null, prefix: null };
1187
+ }
1188
+
1189
+ return splitS3BucketPath(text);
1190
+ }
1191
+
1192
+ /**
1193
+ * CDN prefix + S3 upload (`build.assets`).
1194
+ *
1195
+ * Prefix / bucket / keyPrefix / region / endpoint **yalnızca config**'ten.
1196
+ * Credential env'den (`AWS_*`, yoksa `JSKELET_S3_*`) — config'e yazılmaz.
1190
1197
  *
1191
1198
  * @param {unknown} raw
1192
1199
  * @returns {BuildAssetsConfig}
@@ -1201,10 +1208,9 @@ export function normalizeBuildAssets(raw) {
1201
1208
  console.warn("[config] build.assets must be an object, ignoring it");
1202
1209
  }
1203
1210
 
1204
- const envPrefix = envText(process.env.JSKELET_ASSET_PREFIX);
1205
- const configPrefix =
1206
- typeof source.prefix === "string" ? source.prefix.trim() : "";
1207
- const prefix = (envPrefix ?? configPrefix).replace(/\/+$/, "");
1211
+ const prefix = (
1212
+ typeof source.prefix === "string" ? source.prefix.trim() : ""
1213
+ ).replace(/\/+$/, "");
1208
1214
 
1209
1215
  const uploadRaw =
1210
1216
  source.upload != null &&
@@ -1220,10 +1226,7 @@ export function normalizeBuildAssets(raw) {
1220
1226
  console.warn("[config] build.assets.upload must be an object, ignoring it");
1221
1227
  }
1222
1228
 
1223
- const envBucket = envText(process.env.JSKELET_ASSETS_BUCKET);
1224
- const bucketPath = splitS3BucketPath(
1225
- envBucket ?? envText(uploadRaw.bucket),
1226
- );
1229
+ const bucketPath = normalizeAssetsBucket(uploadRaw.bucket);
1227
1230
 
1228
1231
  const configKeyPrefix =
1229
1232
  typeof uploadRaw.keyPrefix === "string" && uploadRaw.keyPrefix
@@ -1236,17 +1239,15 @@ export function normalizeBuildAssets(raw) {
1236
1239
  : `${uploadRaw.prefix}/`
1237
1240
  : DEFAULT_BUILD.assets.upload.keyPrefix;
1238
1241
 
1239
- const endpoint =
1240
- envText(process.env.AWS_ENDPOINT_URL) ??
1241
- envText(process.env.JSKELET_S3_API_URL) ??
1242
- envText(uploadRaw.endpoint);
1242
+ // Bucket yolundaki önek (`my-bucket/js`) `keyPrefix`'i ezer — tek alanda
1243
+ // hem kova hem klasör vermek için.
1244
+ const keyPrefix = bucketPath.prefix ?? configKeyPrefix;
1245
+
1246
+ const endpoint = envText(uploadRaw.endpoint);
1243
1247
 
1244
1248
  // Gerçek AWS'te region imza için gerekir; custom endpoint'te `auto` yaygın.
1245
1249
  const region =
1246
1250
  envText(uploadRaw.region) ??
1247
- envText(process.env.AWS_REGION) ??
1248
- envText(process.env.AWS_DEFAULT_REGION) ??
1249
- envText(process.env.JSKELET_S3_REGION) ??
1250
1251
  (endpoint ? "auto" : DEFAULT_BUILD.assets.upload.region);
1251
1252
 
1252
1253
  /** @type {string[]} */
@@ -1268,8 +1269,7 @@ export function normalizeBuildAssets(raw) {
1268
1269
  ? uploadRaw.cacheControl.trim()
1269
1270
  : DEFAULT_BUILD.assets.upload.cacheControl;
1270
1271
 
1271
- // `JSKELET_ASSETS_BUCKET` doluysa config'te `enabled` unutulmuş olsa bile aç.
1272
- // Açık `enabled: false` ezer.
1272
+ // Config'te bucket varsa `enabled` unutulmuş olsa bile aç. Açık `false` ezer.
1273
1273
  const enabled =
1274
1274
  uploadRaw.enabled === true ||
1275
1275
  (uploadRaw.enabled !== false && Boolean(bucketPath.bucket));
@@ -1279,7 +1279,7 @@ export function normalizeBuildAssets(raw) {
1279
1279
  upload: {
1280
1280
  enabled,
1281
1281
  bucket: bucketPath.bucket,
1282
- keyPrefix: bucketPath.prefix ?? configKeyPrefix,
1282
+ keyPrefix,
1283
1283
  region,
1284
1284
  endpoint,
1285
1285
  include,
@@ -6,9 +6,9 @@
6
6
  * basmaz. Böylece `jskelet build` unutulduğunda hata yerine stilsiz ama
7
7
  * çalışan bir sayfa görürsünüz.
8
8
  *
9
- * `build.assets.prefix` / `JSKELET_ASSET_PREFIX` doluysa HTML'e basılan
10
- * URL'lere CDN origin eklenir; manifest diskte göreli kalır (çok ortamlı
11
- * tek artifact). Dosya sistemi okumaları (`getSpriteIds`) göreli yolu kullanır.
9
+ * `build.assets.prefix` doluysa HTML'e basılan URL'lere CDN origin eklenir;
10
+ * manifest diskte göreli kalır. Dosya sistemi okumaları (`getSpriteIds`)
11
+ * göreli yolu kullanır.
12
12
  */
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
@@ -6,9 +6,9 @@
6
6
  */
7
7
  import process from "node:process";
8
8
  import * as log from "../../log.mjs";
9
- import { readS3CredentialsFromEnv } from "../../config/index.js";
10
9
  import { createFileSink } from "./file-sink.js";
11
10
  import { createS3Sink } from "./s3-sink.js";
11
+ import { resolveS3Credentials } from "./s3-credentials.js";
12
12
 
13
13
  /**
14
14
  * @typedef {import('../../config/index.js').LogsConfig} LogsConfig
@@ -73,14 +73,15 @@ export async function configureLogs(config) {
73
73
  }
74
74
 
75
75
  if (logs.s3.enabled) {
76
- const credentials = readS3CredentialsFromEnv();
76
+ const credentials = await resolveS3Credentials();
77
77
  if (!logs.s3.bucket) {
78
78
  console.warn(
79
79
  "[logs] s3.enabled but no bucket (set JSKELET_LOG_BUCKET, JSKELET_S3_BUCKET or logs.s3.bucket)",
80
80
  );
81
81
  } else if (!credentials) {
82
82
  console.warn(
83
- "[logs] s3.enabled but credentials missing — set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or JSKELET_S3_*",
83
+ "[logs] s3.enabled but credentials missing — set AWS_ACCESS_KEY_ID/" +
84
+ "AWS_SECRET_ACCESS_KEY or JSKELET_S3_*, or run with an ECS/EC2 IAM role",
84
85
  );
85
86
  } else {
86
87
  const region = logs.s3.region || "auto";
@@ -0,0 +1,195 @@
1
+ /**
2
+ * S3 imza credential zinciri — SDK yok.
3
+ *
4
+ * 1. Env: `AWS_*` sonra `JSKELET_S3_*`
5
+ * 2. ECS/Fargate task role: `AWS_CONTAINER_CREDENTIALS_*`
6
+ * 3. EC2 instance profile: IMDSv2 (kısa timeout; local'de asılı kalmaz)
7
+ *
8
+ * EKS IRSA (`AWS_WEB_IDENTITY_TOKEN_FILE`) bilinçli olarak yok — STS çağrısı
9
+ * ayrı yüzey; CI genelde zaten `AWS_*` basar.
10
+ */
11
+ import fs from "node:fs";
12
+ import process from "node:process";
13
+
14
+ /**
15
+ * @typedef {{ accessKeyId: string, secretAccessKey: string,
16
+ * sessionToken: string | null }} AwsCredentials
17
+ */
18
+
19
+ const METADATA_TIMEOUT_MS = 400;
20
+ const ECS_METADATA_HOST = "http://169.254.170.2";
21
+ const EC2_METADATA_HOST = "http://169.254.169.254";
22
+
23
+ /**
24
+ * @param {unknown} value
25
+ * @returns {string | null}
26
+ */
27
+ function envText(value) {
28
+ return typeof value === "string" && value ? value : null;
29
+ }
30
+
31
+ /**
32
+ * @returns {AwsCredentials | null}
33
+ */
34
+ export function readS3CredentialsFromEnv() {
35
+ const accessKeyId =
36
+ envText(process.env.AWS_ACCESS_KEY_ID) ??
37
+ envText(process.env.JSKELET_S3_ACCESS_KEY_ID);
38
+ const secretAccessKey =
39
+ envText(process.env.AWS_SECRET_ACCESS_KEY) ??
40
+ envText(process.env.JSKELET_S3_SECRET_ACCESS_KEY) ??
41
+ envText(process.env.JSKELET_S3_ACCESS_SECRET);
42
+ if (!accessKeyId || !secretAccessKey) return null;
43
+ return {
44
+ accessKeyId,
45
+ secretAccessKey,
46
+ sessionToken:
47
+ envText(process.env.AWS_SESSION_TOKEN) ??
48
+ envText(process.env.JSKELET_S3_SESSION_TOKEN),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * @param {Record<string, unknown>} data
54
+ * @returns {AwsCredentials | null}
55
+ */
56
+ function fromAwsCredentialJson(data) {
57
+ const accessKeyId =
58
+ typeof data.AccessKeyId === "string" ? data.AccessKeyId : null;
59
+ const secretAccessKey =
60
+ typeof data.SecretAccessKey === "string" ? data.SecretAccessKey : null;
61
+ if (!accessKeyId || !secretAccessKey) return null;
62
+ const token =
63
+ (typeof data.Token === "string" && data.Token) ||
64
+ (typeof data.SessionToken === "string" && data.SessionToken) ||
65
+ null;
66
+ return { accessKeyId, secretAccessKey, sessionToken: token };
67
+ }
68
+
69
+ /**
70
+ * @param {string} url
71
+ * @param {RequestInit} [init]
72
+ * @param {number} [timeoutMs]
73
+ * @returns {Promise<unknown | null>}
74
+ */
75
+ async function fetchJson(url, init = {}, timeoutMs = METADATA_TIMEOUT_MS) {
76
+ const controller = new AbortController();
77
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
78
+ try {
79
+ const response = await fetch(url, { ...init, signal: controller.signal });
80
+ if (!response.ok) return null;
81
+ return await response.json();
82
+ } catch {
83
+ return null;
84
+ } finally {
85
+ clearTimeout(timer);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {string} url
91
+ * @param {RequestInit} [init]
92
+ * @param {number} [timeoutMs]
93
+ * @returns {Promise<string | null>}
94
+ */
95
+ async function fetchText(url, init = {}, timeoutMs = METADATA_TIMEOUT_MS) {
96
+ const controller = new AbortController();
97
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
98
+ try {
99
+ const response = await fetch(url, { ...init, signal: controller.signal });
100
+ if (!response.ok) return null;
101
+ return (await response.text()).trim() || null;
102
+ } catch {
103
+ return null;
104
+ } finally {
105
+ clearTimeout(timer);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * ECS task role / container credential provider.
111
+ *
112
+ * @returns {Promise<AwsCredentials | null>}
113
+ */
114
+ export async function readS3CredentialsFromEcs() {
115
+ const relative = envText(process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI);
116
+ const full = envText(process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI);
117
+ if (!relative && !full) return null;
118
+
119
+ const url = full ?? `${ECS_METADATA_HOST}${relative}`;
120
+
121
+ /** @type {Record<string, string>} */
122
+ const headers = {};
123
+ const token =
124
+ envText(process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN) ??
125
+ readTokenFile(process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE);
126
+ if (token) headers.Authorization = token;
127
+
128
+ const data = await fetchJson(url, { headers });
129
+ if (!data || typeof data !== "object") return null;
130
+ return fromAwsCredentialJson(/** @type {Record<string, unknown>} */ (data));
131
+ }
132
+
133
+ /**
134
+ * @param {unknown} pathValue
135
+ * @returns {string | null}
136
+ */
137
+ function readTokenFile(pathValue) {
138
+ const file = envText(pathValue);
139
+ if (!file) return null;
140
+ try {
141
+ const text = fs.readFileSync(file, "utf8").trim();
142
+ return text || null;
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * EC2 instance profile via IMDSv2 (IMDSv1'e düşmez).
150
+ *
151
+ * @returns {Promise<AwsCredentials | null>}
152
+ */
153
+ export async function readS3CredentialsFromImds() {
154
+ const token = await fetchText(
155
+ `${EC2_METADATA_HOST}/latest/api/token`,
156
+ {
157
+ method: "PUT",
158
+ headers: { "X-aws-ec2-metadata-token-ttl-seconds": "21600" },
159
+ },
160
+ );
161
+ if (!token) return null;
162
+
163
+ const headers = { "X-aws-ec2-metadata-token": token };
164
+ const roleName = await fetchText(
165
+ `${EC2_METADATA_HOST}/latest/meta-data/iam/security-credentials/`,
166
+ { headers },
167
+ );
168
+ if (!roleName) return null;
169
+
170
+ // Birden fazla satır olursa ilki.
171
+ const role = roleName.split("\n")[0]?.trim();
172
+ if (!role || role.includes("..") || role.includes("/")) return null;
173
+
174
+ const data = await fetchJson(
175
+ `${EC2_METADATA_HOST}/latest/meta-data/iam/security-credentials/${encodeURIComponent(role)}`,
176
+ { headers },
177
+ );
178
+ if (!data || typeof data !== "object") return null;
179
+ return fromAwsCredentialJson(/** @type {Record<string, unknown>} */ (data));
180
+ }
181
+
182
+ /**
183
+ * Env → ECS task role → EC2 instance profile.
184
+ *
185
+ * @returns {Promise<AwsCredentials | null>}
186
+ */
187
+ export async function resolveS3Credentials() {
188
+ const fromEnv = readS3CredentialsFromEnv();
189
+ if (fromEnv) return fromEnv;
190
+
191
+ const fromEcs = await readS3CredentialsFromEcs();
192
+ if (fromEcs) return fromEcs;
193
+
194
+ return readS3CredentialsFromImds();
195
+ }
@@ -2,9 +2,8 @@
2
2
  * Minimal S3 PutObject — yalnızca SigV4 + `fetch`.
3
3
  *
4
4
  * `@aws-sdk/client-s3` taşınmıyor: tek operasyon için SDK'nın maliyeti ve
5
- * peer yüzeyi gereksiz. Credential zinciri env'den okunur (`AWS_*` önce,
6
- * sonra `JSKELET_S3_*`); IAM role / instance metadata yok — CI'da
7
- * `configure-aws-credentials` gibi araçlar key basar.
5
+ * peer yüzeyi gereksiz. Credential `resolveS3Credentials` ile gelir
6
+ * (env → ECS task role → EC2 IMDS).
8
7
  */
9
8
  import crypto from "node:crypto";
10
9
 
@@ -448,10 +448,11 @@ export declare const DEFAULT_BUILD: {
448
448
  chunkGroup: ((id: string) => string | null | undefined) | null;
449
449
  };
450
450
  assets: {
451
- /** CDN / CloudFront origin; sonunda `/` yok. Env: `JSKELET_ASSET_PREFIX`. */
451
+ /** CDN / CloudFront origin; sonunda `/` yok. Yalnızca config. */
452
452
  prefix: string;
453
453
  upload: {
454
454
  enabled: boolean;
455
+ /** S3 bucket adı (CDN URL değil). Yalnızca config; credential env'den. */
455
456
  bucket: null;
456
457
  /** Nesne anahtarı öneki (`bucket/path` split'inden veya config). */
457
458
  keyPrefix: string;
@@ -1,4 +1,6 @@
1
+ import { readS3CredentialsFromEnv, resolveS3Credentials } from "../server/logs/s3-credentials.js";
1
2
  import { DEFAULT_ADMIN, DEFAULT_CLOUDFLARE, DEFAULT_UPSTREAM_LIMIT } from "./defaults.js";
3
+ export { readS3CredentialsFromEnv, resolveS3Credentials };
2
4
  /** Framework paketinin kökü — kendi şablonlarına ve varlıklarına erişir. */
3
5
  export declare const FRAMEWORK_ROOT: string;
4
6
  export type Eagerness = "conservative" | "moderate" | "eager";
@@ -241,7 +243,7 @@ export type BuildAssetsUploadConfig = {
241
243
  };
242
244
  export type BuildAssetsConfig = {
243
245
  /**
244
- * CDN origin (`JSKELET_ASSET_PREFIX`); sonunda `/` yok.
246
+ * CDN origin (`build.assets.prefix`); sonunda `/` yok.
245
247
  */
246
248
  prefix: string;
247
249
  upload: BuildAssetsUploadConfig;
@@ -278,19 +280,6 @@ export declare function splitS3BucketPath(value: string | null): {
278
280
  bucket: string | null;
279
281
  prefix: string | null;
280
282
  };
281
- /**
282
- * S3 imza credential'ları. AWS deploy'da standart env önce gelir; R2/MinIO
283
- * için `JSKELET_S3_*` yedek. IAM instance/task role (IMDS) desteklenmez —
284
- * CI'da `configure-aws-credentials` gibi araçlar zaten `AWS_*` basar.
285
- *
286
- * @returns {{ accessKeyId: string, secretAccessKey: string,
287
- * sessionToken: string | null } | null}
288
- */
289
- export declare function readS3CredentialsFromEnv(): {
290
- accessKeyId: string;
291
- secretAccessKey: string;
292
- sessionToken: string | null;
293
- } | null;
294
283
  /**
295
284
  * Kalıcı log sink'leri. Bozuk bir `kinds` listesi siteyi düşürmemeli —
296
285
  * bilinmeyen girdiler atılır; hiç geçerli tür kalmazsa varsayılana dönülür.
@@ -313,7 +302,10 @@ export type SecurityConfig = {
313
302
  };
314
303
  };
315
304
  /**
316
- * CDN prefix + S3 upload (`build.assets`). Credential config'e yazılmaz.
305
+ * CDN prefix + S3 upload (`build.assets`).
306
+ *
307
+ * Prefix / bucket / keyPrefix / region / endpoint **yalnızca config**'ten.
308
+ * Credential env'den (`AWS_*`, yoksa `JSKELET_S3_*`) — config'e yazılmaz.
317
309
  *
318
310
  * @param {unknown} raw
319
311
  * @returns {BuildAssetsConfig}
@@ -0,0 +1,27 @@
1
+ export type AwsCredentials = {
2
+ accessKeyId: string;
3
+ secretAccessKey: string;
4
+ sessionToken: string | null;
5
+ };
6
+ /**
7
+ * @returns {AwsCredentials | null}
8
+ */
9
+ export declare function readS3CredentialsFromEnv(): AwsCredentials | null;
10
+ /**
11
+ * ECS task role / container credential provider.
12
+ *
13
+ * @returns {Promise<AwsCredentials | null>}
14
+ */
15
+ export declare function readS3CredentialsFromEcs(): Promise<AwsCredentials | null>;
16
+ /**
17
+ * EC2 instance profile via IMDSv2 (IMDSv1'e düşmez).
18
+ *
19
+ * @returns {Promise<AwsCredentials | null>}
20
+ */
21
+ export declare function readS3CredentialsFromImds(): Promise<AwsCredentials | null>;
22
+ /**
23
+ * Env → ECS task role → EC2 instance profile.
24
+ *
25
+ * @returns {Promise<AwsCredentials | null>}
26
+ */
27
+ export declare function resolveS3Credentials(): Promise<AwsCredentials | null>;