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 +8 -5
- package/docs/07-yapilandirma.md +7 -7
- package/docs/08-build.md +2 -2
- package/docs/10-dagitim.md +23 -16
- package/docs/en/07-configuration.md +8 -7
- package/docs/en/08-build.md +2 -2
- package/docs/en/10-deployment.md +22 -16
- package/package.json +1 -1
- package/src/build/tasks/assets-upload.mjs +18 -12
- package/src/config/index.js +6 -26
- package/src/server/logs/pipeline.js +4 -3
- package/src/server/logs/s3-credentials.js +212 -0
- package/src/server/logs/s3-put.js +2 -3
- package/types/config/index.d.ts +2 -13
- package/types/server/logs/s3-credentials.d.ts +33 -0
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_
|
|
20
|
-
when off it logs a
|
|
21
|
-
`skipped (
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -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
|
-
**
|
|
642
|
-
`AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` (yoksa `JSKELET_S3_*`)
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
yok. `.br` / `.gz` yüklenmez.
|
|
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;
|
|
88
|
-
|
|
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.
|
package/docs/10-dagitim.md
CHANGED
|
@@ -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;
|
|
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:
|
|
65
|
+
prefix: process.env.NEXT_PUBLIC_STATIC_ASSETS_BASE_URL, // https://static.…
|
|
65
66
|
upload: {
|
|
66
|
-
bucket:
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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/`
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
**
|
|
655
|
-
`AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` (else `JSKELET_S3_*`)
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
No upload on watch passes. `.br` / `.gz` siblings are
|
|
660
|
-
[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),
|
|
661
|
+
[10-deployment.md](./10-deployment.md).
|
|
661
662
|
|
|
662
663
|
```js
|
|
663
664
|
build: {
|
package/docs/en/08-build.md
CHANGED
|
@@ -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;
|
|
91
|
-
|
|
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.
|
package/docs/en/10-deployment.md
CHANGED
|
@@ -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
|
|
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:
|
|
66
|
+
prefix: process.env.NEXT_PUBLIC_STATIC_ASSETS_BASE_URL, // https://static.…
|
|
66
67
|
upload: {
|
|
67
|
-
bucket:
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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,
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
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
|
|
5
|
-
*
|
|
6
|
-
* kapalıysa adım yine görünür: `skipped (…)` ile sebep basılır
|
|
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 {
|
|
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
|
-
*
|
|
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
|
|
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
|
|
127
|
-
|
|
130
|
+
const resolve = options.resolveCredentials ?? resolveS3Credentials;
|
|
131
|
+
const credentials = await resolve();
|
|
128
132
|
if (!upload.bucket || !credentials) {
|
|
129
|
-
log.detail(
|
|
133
|
+
log.detail(`skipped (${s3CredentialsMissingReason()})`);
|
|
130
134
|
return;
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
const put = options.put ?? putObject;
|
|
134
|
-
|
|
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 = [];
|
package/src/config/index.js
CHANGED
|
@@ -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 =
|
|
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/
|
|
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
|
|
6
|
-
*
|
|
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
|
|
package/types/config/index.d.ts
CHANGED
|
@@ -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>;
|