jskelet 0.6.10 → 0.6.12

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
@@ -16,11 +16,14 @@ one is listed under a **Breaking** heading.
16
16
  Production `jskelet build` PutObjects `public/assets/` (and optional
17
17
  `include` dirs) after precompress. Only credentials are read from env
18
18
  (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, then
19
- `JSKELET_S3_*`). The Assets upload step always appears on production builds;
20
- when off it logs a one-line reason (`skipped (no build.assets.upload.bucket)`,
21
- `skipped (credentials missing)`, …). A URL in `upload.bucket` is rejected
22
- with a warning (CDN origin belongs in `prefix`). Watch mode never uploads.
23
- `.br` / `.gz` siblings are skipped.
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 (no env keys, no ECS task role …))`, …).
23
+ Docker/`RUN jskelet build` has no task role — upload needs CI role or a
24
+ runtime upload. A URL in `upload.bucket` is rejected with a warning (CDN
25
+ origin belongs in `prefix`). Watch mode never uploads. `.br` / `.gz`
26
+ siblings are skipped.
24
27
 
25
28
  ### Changed
26
29
 
@@ -638,13 +638,13 @@ değişmez** — asset'ler origin'den servis edilir.
638
638
  | `assets.upload.include` | `string[]` | `["assets"]` | `public/` altı dizinler (`assets`, isteğe `fonts`, …). |
639
639
  | `assets.upload.cacheControl` | `string` | `public, max-age=31536000, immutable` | PutObject `Cache-Control`. |
640
640
 
641
- **Yalnızca credential env'den gelir:** `AWS_ACCESS_KEY_ID` /
642
- `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` (yoksa `JSKELET_S3_*`). Prefix,
643
- bucket, region vb. config'te yazılır. Production build'de Assets upload adımı
644
- her zaman görünür; kapalıysa `skipped (no build.assets.upload.bucket)` /
645
- `skipped (credentials missing)` gibi tek satır sebep basılır. Watch'ta adım
646
- yok. `.br` / `.gz` yüklenmez. Ayrıntı: [08-build.md](./08-build.md),
647
- [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).
648
648
 
649
649
  ```js
650
650
  build: {
package/docs/08-build.md CHANGED
@@ -84,8 +84,8 @@ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
84
84
 
85
85
  ### CDN prefix ve S3 upload
86
86
 
87
- Varsayılan kapalıdır. Prefix / bucket / keyPrefix config'te; yalnızca
88
- credential env'den (`AWS_*`, yoksa `JSKELET_S3_*`). 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:
89
89
 
90
90
  1. Production build `public/assets/` (ve `include` listesindeki diğer dizinler)
91
91
  altındaki dosyaları S3'e PutObject ile kopyalar.
@@ -55,34 +55,41 @@ ayarlamayı düşünmeniz gerekenler:
55
55
 
56
56
  ### Asset'leri S3 / CloudFront'a basmak
57
57
 
58
- Varsayılan kapalıdır. Config'te prefix + bucket; CI'da yalnızca credential:
58
+ Varsayılan kapalıdır. Config'te prefix + bucket; credential env key **veya**
59
+ IAM role:
59
60
 
60
61
  ```js
61
- // jskelet.config.mjs
62
+ // jskelet.config.mjs — uygulama env'lerini buraya bağla
62
63
  build: {
63
64
  assets: {
64
- prefix: "https://static.ornek.com/js",
65
+ prefix: process.env.NEXT_PUBLIC_STATIC_ASSETS_BASE_URL, // https://static.…
65
66
  upload: {
66
- bucket: "static.ornek.com", // veya "static.ornek.com/js"
67
+ bucket: process.env.S3_BUCKET_NAME, // static.investvio.com
67
68
  keyPrefix: "js",
68
- region: "eu-central-1",
69
+ region: process.env.AWS_REGION || "eu-central-1",
70
+ endpoint: process.env.AWS_S3_ENDPOINT || undefined,
69
71
  },
70
72
  },
71
73
  },
72
74
  ```
73
75
 
74
- ```bash
75
- AWS_ACCESS_KEY_ID=…
76
- AWS_SECRET_ACCESS_KEY=…
77
- # geçici credential ise:
78
- # AWS_SESSION_TOKEN=…
79
- ```
76
+ **Task role ne zaman çalışır?** ECS task role /
77
+ `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` yalnızca **görev çalışırken** vardır.
78
+ Dockerfile içindeki `RUN jskelet build` veya CI build agent'ında bu URI
79
+ yoktur → `skipped (credentials missing (no env keys, no ECS task role …))`.
80
+ AWS SDK'nın runtime'da rolü görmesi, build adımının rolü görmesi demek değildir.
81
+
82
+ Pratik seçenekler:
83
+
84
+ 1. **CI'da rol ile build** (CodeBuild / GitHub OIDC) — static key yok, upload
85
+ build job'unda biter.
86
+ 2. **Image'da yalnızca derle**, upload'u ECS start'ta ayrı koş (task role o an
87
+ vardır).
88
+ 3. CI'da geçici `AWS_*` key (tercih edilmez).
80
89
 
81
- `jskelet build` bitince `public/assets/` (ve config'te `include` edilen
82
- dizinler) S3'e kopyalanır; origin'deki dosyalar da durur. Bucket veya
83
- credential yoksa adım `skipped (…)` ile sebep basar, build devam eder. Watch /
84
- `jskelet dev` upload yapmaz. CSP kullanıyorsanız CDN origin'ini `script-src` /
85
- `style-src` / `img-src` ve `preconnect` listesine ekleyin.
90
+ `jskelet build` bitince `public/assets/` S3'e kopyalanır; origin kopyası durur.
91
+ Bucket/credential yoksa adım `skipped (…)` basar. Watch / `jskelet dev` upload
92
+ yapmaz. CSP'ye CDN origin ekleyin.
86
93
 
87
94
  Production'da dosya veya S3 sink açıldığında HTTP access log middleware
88
95
  otomatik mount edilir (`logs.kinds` içinde `http` varsa). Admin paneli ring'i
@@ -651,13 +651,14 @@ changes** — assets stay on the origin.
651
651
  | `assets.upload.include` | `string[]` | `["assets"]` | Directories under `public/` (`assets`, optionally `fonts`, …). |
652
652
  | `assets.upload.cacheControl` | `string` | `public, max-age=31536000, immutable` | PutObject `Cache-Control`. |
653
653
 
654
- **Only credentials come from the environment:** `AWS_ACCESS_KEY_ID` /
655
- `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` (else `JSKELET_S3_*`). Prefix,
656
- bucket, region, etc. are written in the config. On a production build the Assets
657
- upload step always appears; when off it logs a one-line reason such as
658
- `skipped (no build.assets.upload.bucket)` or `skipped (credentials missing)`.
659
- No upload on watch passes. `.br` / `.gz` siblings are not uploaded. Details:
660
- [08-build.md](./08-build.md), [10-deployment.md](./10-deployment.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),
661
+ [10-deployment.md](./10-deployment.md).
661
662
 
662
663
  ```js
663
664
  build: {
@@ -87,8 +87,8 @@ They are passed to templates automatically; in server code,
87
87
 
88
88
  ### CDN prefix and S3 upload
89
89
 
90
- Off by default. Prefix / bucket / keyPrefix come from config; only credentials
91
- come from the environment (`AWS_*`, else `JSKELET_S3_*`). 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.
@@ -56,34 +56,40 @@ considering in production:
56
56
 
57
57
  ### Publishing assets to S3 / CloudFront
58
58
 
59
- Off by default. Prefix + bucket in config; credentials only in CI:
59
+ Off by default. Prefix + bucket in config; credentials from env keys **or** an
60
+ IAM role:
60
61
 
61
62
  ```js
62
- // jskelet.config.mjs
63
+ // jskelet.config.mjs — wire your app env vars here
63
64
  build: {
64
65
  assets: {
65
- prefix: "https://static.example.com/js",
66
+ prefix: process.env.NEXT_PUBLIC_STATIC_ASSETS_BASE_URL, // https://static.…
66
67
  upload: {
67
- bucket: "static.example.com", // or "static.example.com/js"
68
+ bucket: process.env.S3_BUCKET_NAME, // static.example.com
68
69
  keyPrefix: "js",
69
- region: "eu-central-1",
70
+ region: process.env.AWS_REGION || "eu-central-1",
71
+ endpoint: process.env.AWS_S3_ENDPOINT || undefined,
70
72
  },
71
73
  },
72
74
  },
73
75
  ```
74
76
 
75
- ```bash
76
- AWS_ACCESS_KEY_ID=…
77
- AWS_SECRET_ACCESS_KEY=…
78
- # for temporary credentials:
79
- # AWS_SESSION_TOKEN=…
80
- ```
77
+ **When does the task role work?** The ECS task role /
78
+ `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` exist only while the **task is
79
+ running**. A `RUN jskelet build` inside Docker or a CI build agent has no such
80
+ URI → `skipped (credentials missing (no env keys, no ECS task role …))`. The AWS
81
+ SDK seeing the role at runtime does not mean the build step sees it.
82
+
83
+ Practical options:
84
+
85
+ 1. **Build with a CI role** (CodeBuild / GitHub OIDC) — no static keys; upload
86
+ finishes in the job.
87
+ 2. **Compile in the image**, run upload at ECS start (task role is present then).
88
+ 3. Temporary `AWS_*` keys in CI (not preferred).
81
89
 
82
- When `jskelet build` finishes, files under `public/assets/` (and any other
83
- `include` directories) are copied to S3; origin copies remain. If the bucket or
84
- credentials are missing, the step logs `skipped (…)` with the reason and the
85
- build continues. Watch / `jskelet dev` never upload. If you use CSP, add the CDN
86
- origin to `script-src` / `style-src` / `img-src` and to `preconnect`.
90
+ When `jskelet build` finishes, `public/assets/` is copied to S3; origin copies
91
+ remain. Missing bucket/credentials log `skipped (…)`. Watch / `jskelet dev`
92
+ never upload. Add the CDN origin to CSP.
87
93
 
88
94
  When a file or S3 sink is enabled in production, the HTTP access log middleware
89
95
  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.6.10",
3
+ "version": "0.6.12",
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,21 @@
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 {
16
+ resolveS3Credentials,
17
+ s3CredentialsMissingReason,
18
+ } from "../../server/logs/s3-credentials.js";
14
19
  import { putObject } from "../../server/logs/s3-put.js";
15
20
  import * as log from "../../log.mjs";
16
21
  import { paths } from "../paths.mjs";
@@ -88,7 +93,7 @@ function resolveIncludeDir(name) {
88
93
  }
89
94
 
90
95
  /**
91
- * Upload yapılmayacaksa kısa sebep; yapılacaksa `null`.
96
+ * Config kapısı — credential burada bakılmaz (task role async).
92
97
  *
93
98
  * @param {import('../../config/index.js').ResolvedConfig} config
94
99
  * @returns {string | null}
@@ -105,14 +110,13 @@ export function assetsUploadSkipReason(config) {
105
110
 
106
111
  if (!upload.bucket) return "no build.assets.upload.bucket";
107
112
 
108
- if (!readS3CredentialsFromEnv()) return "credentials missing";
109
-
110
113
  return null;
111
114
  }
112
115
 
113
116
  /**
114
117
  * @param {import('../../config/index.js').ResolvedConfig} config
115
- * @param {{ put?: typeof putObject }} [options]
118
+ * @param {{ put?: typeof putObject,
119
+ * resolveCredentials?: typeof resolveS3Credentials }} [options]
116
120
  * @returns {Promise<void>}
117
121
  */
118
122
  export async function uploadAssets(config, options = {}) {
@@ -123,15 +127,17 @@ export async function uploadAssets(config, options = {}) {
123
127
  }
124
128
 
125
129
  const upload = config.build.assets.upload;
126
- const credentials = readS3CredentialsFromEnv();
127
- // skipReason null iken credential ve bucket vardır; TS/akış için daralt.
130
+ const resolve = options.resolveCredentials ?? resolveS3Credentials;
131
+ const credentials = await resolve();
128
132
  if (!upload.bucket || !credentials) {
129
- log.detail("skipped (credentials missing)");
133
+ log.detail(`skipped (${s3CredentialsMissingReason()})`);
130
134
  return;
131
135
  }
132
136
 
133
137
  const put = options.put ?? putObject;
134
- const keyPrefix = (upload.keyPrefix || "").replace(/^\/+/, "");
138
+ // Normalize bazen atlanır (test / elle config); önek `/` ile bitsin.
139
+ let keyPrefix = (upload.keyPrefix || "").replace(/^\/+/, "");
140
+ if (keyPrefix && !keyPrefix.endsWith("/")) keyPrefix += "/";
135
141
 
136
142
  /** @type {{ file: string, key: string }[]} */
137
143
  const jobs = [];
@@ -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
 
@@ -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`).
@@ -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,212 @@
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
+ // Local'de IMDS yokken hızlı vazgeç; ECS'te URI varken biraz daha sabır.
20
+ const IMDS_TIMEOUT_MS = 400;
21
+ const ECS_TIMEOUT_MS = 2_000;
22
+ const ECS_METADATA_HOST = "http://169.254.170.2";
23
+ const EC2_METADATA_HOST = "http://169.254.169.254";
24
+
25
+ /**
26
+ * @param {unknown} value
27
+ * @returns {string | null}
28
+ */
29
+ function envText(value) {
30
+ return typeof value === "string" && value ? value : null;
31
+ }
32
+
33
+ /**
34
+ * @returns {AwsCredentials | null}
35
+ */
36
+ export function readS3CredentialsFromEnv() {
37
+ const accessKeyId =
38
+ envText(process.env.AWS_ACCESS_KEY_ID) ??
39
+ envText(process.env.JSKELET_S3_ACCESS_KEY_ID);
40
+ const secretAccessKey =
41
+ envText(process.env.AWS_SECRET_ACCESS_KEY) ??
42
+ envText(process.env.JSKELET_S3_SECRET_ACCESS_KEY) ??
43
+ envText(process.env.JSKELET_S3_ACCESS_SECRET);
44
+ if (!accessKeyId || !secretAccessKey) return null;
45
+ return {
46
+ accessKeyId,
47
+ secretAccessKey,
48
+ sessionToken:
49
+ envText(process.env.AWS_SESSION_TOKEN) ??
50
+ envText(process.env.JSKELET_S3_SESSION_TOKEN),
51
+ };
52
+ }
53
+
54
+ /**
55
+ * @param {Record<string, unknown>} data
56
+ * @returns {AwsCredentials | null}
57
+ */
58
+ function fromAwsCredentialJson(data) {
59
+ const accessKeyId =
60
+ typeof data.AccessKeyId === "string" ? data.AccessKeyId : null;
61
+ const secretAccessKey =
62
+ typeof data.SecretAccessKey === "string" ? data.SecretAccessKey : null;
63
+ if (!accessKeyId || !secretAccessKey) return null;
64
+ const token =
65
+ (typeof data.Token === "string" && data.Token) ||
66
+ (typeof data.SessionToken === "string" && data.SessionToken) ||
67
+ null;
68
+ return { accessKeyId, secretAccessKey, sessionToken: token };
69
+ }
70
+
71
+ /**
72
+ * @param {string} url
73
+ * @param {RequestInit} [init]
74
+ * @param {number} [timeoutMs]
75
+ * @returns {Promise<unknown | null>}
76
+ */
77
+ async function fetchJson(url, init = {}, timeoutMs = IMDS_TIMEOUT_MS) {
78
+ const controller = new AbortController();
79
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
80
+ try {
81
+ const response = await fetch(url, { ...init, signal: controller.signal });
82
+ if (!response.ok) return null;
83
+ return await response.json();
84
+ } catch {
85
+ return null;
86
+ } finally {
87
+ clearTimeout(timer);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * @param {string} url
93
+ * @param {RequestInit} [init]
94
+ * @param {number} [timeoutMs]
95
+ * @returns {Promise<string | null>}
96
+ */
97
+ async function fetchText(url, init = {}, timeoutMs = IMDS_TIMEOUT_MS) {
98
+ const controller = new AbortController();
99
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
100
+ try {
101
+ const response = await fetch(url, { ...init, signal: controller.signal });
102
+ if (!response.ok) return null;
103
+ return (await response.text()).trim() || null;
104
+ } catch {
105
+ return null;
106
+ } finally {
107
+ clearTimeout(timer);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * ECS task role / container credential provider.
113
+ *
114
+ * @returns {Promise<AwsCredentials | null>}
115
+ */
116
+ export async function readS3CredentialsFromEcs() {
117
+ const relative = envText(process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI);
118
+ const full = envText(process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI);
119
+ if (!relative && !full) return null;
120
+
121
+ const url = full ?? `${ECS_METADATA_HOST}${relative}`;
122
+
123
+ /** @type {Record<string, string>} */
124
+ const headers = {};
125
+ const token =
126
+ envText(process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN) ??
127
+ readTokenFile(process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE);
128
+ if (token) headers.Authorization = token;
129
+
130
+ const data = await fetchJson(url, { headers }, ECS_TIMEOUT_MS);
131
+ if (!data || typeof data !== "object") return null;
132
+ return fromAwsCredentialJson(/** @type {Record<string, unknown>} */ (data));
133
+ }
134
+
135
+ /**
136
+ * @param {unknown} pathValue
137
+ * @returns {string | null}
138
+ */
139
+ function readTokenFile(pathValue) {
140
+ const file = envText(pathValue);
141
+ if (!file) return null;
142
+ try {
143
+ const text = fs.readFileSync(file, "utf8").trim();
144
+ return text || null;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * EC2 instance profile via IMDSv2 (IMDSv1'e düşmez).
152
+ *
153
+ * @returns {Promise<AwsCredentials | null>}
154
+ */
155
+ export async function readS3CredentialsFromImds() {
156
+ const token = await fetchText(
157
+ `${EC2_METADATA_HOST}/latest/api/token`,
158
+ {
159
+ method: "PUT",
160
+ headers: { "X-aws-ec2-metadata-token-ttl-seconds": "21600" },
161
+ },
162
+ );
163
+ if (!token) return null;
164
+
165
+ const headers = { "X-aws-ec2-metadata-token": token };
166
+ const roleName = await fetchText(
167
+ `${EC2_METADATA_HOST}/latest/meta-data/iam/security-credentials/`,
168
+ { headers },
169
+ );
170
+ if (!roleName) return null;
171
+
172
+ // Birden fazla satır olursa ilki.
173
+ const role = roleName.split("\n")[0]?.trim();
174
+ if (!role || role.includes("..") || role.includes("/")) return null;
175
+
176
+ const data = await fetchJson(
177
+ `${EC2_METADATA_HOST}/latest/meta-data/iam/security-credentials/${encodeURIComponent(role)}`,
178
+ { headers },
179
+ );
180
+ if (!data || typeof data !== "object") return null;
181
+ return fromAwsCredentialJson(/** @type {Record<string, unknown>} */ (data));
182
+ }
183
+
184
+ /**
185
+ * Credential neden bulunamadı — upload skip satırı için.
186
+ *
187
+ * @returns {string}
188
+ */
189
+ export function s3CredentialsMissingReason() {
190
+ if (
191
+ envText(process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) ||
192
+ envText(process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI)
193
+ ) {
194
+ return "credentials missing (ECS metadata failed)";
195
+ }
196
+ return "credentials missing (no env keys, no ECS task role — upload needs runtime/CI role, not docker build)";
197
+ }
198
+
199
+ /**
200
+ * Env → ECS task role → EC2 instance profile.
201
+ *
202
+ * @returns {Promise<AwsCredentials | null>}
203
+ */
204
+ export async function resolveS3Credentials() {
205
+ const fromEnv = readS3CredentialsFromEnv();
206
+ if (fromEnv) return fromEnv;
207
+
208
+ const fromEcs = await readS3CredentialsFromEcs();
209
+ if (fromEcs) return fromEcs;
210
+
211
+ return readS3CredentialsFromImds();
212
+ }
@@ -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
 
@@ -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";
@@ -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.
@@ -0,0 +1,33 @@
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
+ * Credential neden bulunamadı — upload skip satırı için.
24
+ *
25
+ * @returns {string}
26
+ */
27
+ export declare function s3CredentialsMissingReason(): string;
28
+ /**
29
+ * Env → ECS task role → EC2 instance profile.
30
+ *
31
+ * @returns {Promise<AwsCredentials | null>}
32
+ */
33
+ export declare function resolveS3Credentials(): Promise<AwsCredentials | null>;