jskelet 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +136 -136
- package/CHANGELOG.md +628 -620
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -310
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +667 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1467 -1443
- package/docs/07-yapilandirma.md +1208 -1197
- package/docs/08-build.md +429 -429
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +348 -338
- package/docs/12-panel-ve-oturum.md +479 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -329
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +675 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1476 -1453
- package/docs/en/07-configuration.md +1229 -1219
- package/docs/en/08-build.md +447 -447
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +351 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +489 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +541 -534
- package/src/config/index.js +1500 -1469
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +232 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -70
- package/src/server/cache-control.js +45 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -553
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -233
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1196
- package/src/server/image-optimizer.js +500 -500
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -193
- package/src/server/logs/pipeline.js +165 -165
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -121
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +369 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1082
- package/src/server/redis.js +588 -588
- package/src/server/render.js +910 -910
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +6 -0
- package/types/config/index.d.ts +6 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/og-image.d.ts +5 -0
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal S3 PutObject — yalnızca SigV4 + `fetch`.
|
|
3
|
-
*
|
|
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
|
|
6
|
-
* (`JSKELET_S3_ACCESS_KEY_ID` / `JSKELET_S3_SECRET_ACCESS_KEY` /
|
|
7
|
-
* `JSKELET_S3_SESSION_TOKEN`); IAM role / instance metadata yok —
|
|
8
|
-
* container'a key vermeyen kurulumlar için sink zaten açılışta no-op'a düşer.
|
|
9
|
-
*/
|
|
10
|
-
import crypto from "node:crypto";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @typedef {{ accessKeyId: string, secretAccessKey: string,
|
|
14
|
-
* sessionToken?: string | null }} AwsCredentials
|
|
15
|
-
*
|
|
16
|
-
* @typedef {{ bucket: string, key: string, body: string | Buffer,
|
|
17
|
-
* region: string, endpoint?: string | null, credentials: AwsCredentials,
|
|
18
|
-
* now?: Date }} PutObjectInput
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {string} algorithm
|
|
23
|
-
* @param {string | Buffer} key
|
|
24
|
-
* @param {string | Buffer} data
|
|
25
|
-
* @returns {Buffer}
|
|
26
|
-
*/
|
|
27
|
-
function hmac(algorithm, key, data) {
|
|
28
|
-
return crypto.createHmac(algorithm, key).update(data).digest();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @param {string | Buffer} data
|
|
33
|
-
* @returns {string}
|
|
34
|
-
*/
|
|
35
|
-
export function sha256Hex(data) {
|
|
36
|
-
return crypto.createHash("sha256").update(data).digest("hex");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @param {Date} date
|
|
41
|
-
* @returns {{ amzDate: string, dateStamp: string }}
|
|
42
|
-
*/
|
|
43
|
-
export function formatAmzDates(date) {
|
|
44
|
-
const iso = date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
45
|
-
return {
|
|
46
|
-
amzDate: iso,
|
|
47
|
-
dateStamp: iso.slice(0, 8),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @param {string} secretAccessKey
|
|
53
|
-
* @param {string} dateStamp
|
|
54
|
-
* @param {string} region
|
|
55
|
-
* @param {string} service
|
|
56
|
-
* @returns {Buffer}
|
|
57
|
-
*/
|
|
58
|
-
export function deriveSigningKey(secretAccessKey, dateStamp, region, service) {
|
|
59
|
-
const kDate = hmac("sha256", `AWS4${secretAccessKey}`, dateStamp);
|
|
60
|
-
const kRegion = hmac("sha256", kDate, region);
|
|
61
|
-
const kService = hmac("sha256", kRegion, service);
|
|
62
|
-
return hmac("sha256", kService, "aws4_request");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* @param {{ method: string, canonicalUri: string, canonicalQuerystring?: string,
|
|
67
|
-
* headers: Record<string, string>, payloadHash: string }} parts
|
|
68
|
-
* @returns {{ canonicalRequest: string, signedHeaders: string }}
|
|
69
|
-
*/
|
|
70
|
-
export function buildCanonicalRequest(parts) {
|
|
71
|
-
const names = Object.keys(parts.headers)
|
|
72
|
-
.map((name) => name.toLowerCase())
|
|
73
|
-
.sort();
|
|
74
|
-
const canonicalHeaders = names
|
|
75
|
-
.map((name) => `${name}:${parts.headers[name].trim().replace(/\s+/g, " ")}\n`)
|
|
76
|
-
.join("");
|
|
77
|
-
const signedHeaders = names.join(";");
|
|
78
|
-
|
|
79
|
-
const canonicalRequest = [
|
|
80
|
-
parts.method,
|
|
81
|
-
parts.canonicalUri,
|
|
82
|
-
parts.canonicalQuerystring ?? "",
|
|
83
|
-
canonicalHeaders,
|
|
84
|
-
signedHeaders,
|
|
85
|
-
parts.payloadHash,
|
|
86
|
-
].join("\n");
|
|
87
|
-
|
|
88
|
-
return { canonicalRequest, signedHeaders };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* @param {{ amzDate: string, dateStamp: string, region: string, service: string,
|
|
93
|
-
* canonicalRequest: string }} parts
|
|
94
|
-
* @returns {string}
|
|
95
|
-
*/
|
|
96
|
-
export function buildStringToSign(parts) {
|
|
97
|
-
const scope = `${parts.dateStamp}/${parts.region}/${parts.service}/aws4_request`;
|
|
98
|
-
return [
|
|
99
|
-
"AWS4-HMAC-SHA256",
|
|
100
|
-
parts.amzDate,
|
|
101
|
-
scope,
|
|
102
|
-
sha256Hex(parts.canonicalRequest),
|
|
103
|
-
].join("\n");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* URI path encode (S3 key segment'leri). `/` korunur.
|
|
108
|
-
*
|
|
109
|
-
* @param {string} value
|
|
110
|
-
* @returns {string}
|
|
111
|
-
*/
|
|
112
|
-
export function encodeS3Path(value) {
|
|
113
|
-
return value
|
|
114
|
-
.split("/")
|
|
115
|
-
.map((segment) => encodeURIComponent(segment).replace(/[!'()*]/g, (char) =>
|
|
116
|
-
`%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
117
|
-
))
|
|
118
|
-
.join("/");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* PutObject isteğini imzalar; ağ çağrısı yapmaz — test edilebilir olsun diye.
|
|
123
|
-
*
|
|
124
|
-
* @param {PutObjectInput} input
|
|
125
|
-
* @returns {{ url: string, headers: Record<string, string>, body: Buffer }}
|
|
126
|
-
*/
|
|
127
|
-
export function buildSignedPutObject(input) {
|
|
128
|
-
const body = Buffer.isBuffer(input.body) ? input.body : Buffer.from(input.body);
|
|
129
|
-
const now = input.now ?? new Date();
|
|
130
|
-
const { amzDate, dateStamp } = formatAmzDates(now);
|
|
131
|
-
const service = "s3";
|
|
132
|
-
const payloadHash = sha256Hex(body);
|
|
133
|
-
|
|
134
|
-
const keyPath = encodeS3Path(input.key.replace(/^\/+/, ""));
|
|
135
|
-
const usePathStyle = Boolean(input.endpoint);
|
|
136
|
-
const endpointHost = input.endpoint
|
|
137
|
-
? new URL(input.endpoint).host
|
|
138
|
-
: `${input.bucket}.s3.${input.region}.amazonaws.com`;
|
|
139
|
-
const host = usePathStyle ? new URL(input.endpoint ?? "").host : endpointHost;
|
|
140
|
-
|
|
141
|
-
const canonicalUri = usePathStyle
|
|
142
|
-
? `/${encodeS3Path(input.bucket)}/${keyPath}`
|
|
143
|
-
: `/${keyPath}`;
|
|
144
|
-
|
|
145
|
-
/** @type {Record<string, string>} */
|
|
146
|
-
const headers = {
|
|
147
|
-
host,
|
|
148
|
-
"content-type": "application/x-ndjson",
|
|
149
|
-
"content-length": String(body.length),
|
|
150
|
-
"x-amz-content-sha256": payloadHash,
|
|
151
|
-
"x-amz-date": amzDate,
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
if (input.credentials.sessionToken) {
|
|
155
|
-
headers["x-amz-security-token"] = input.credentials.sessionToken;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const { canonicalRequest, signedHeaders } = buildCanonicalRequest({
|
|
159
|
-
method: "PUT",
|
|
160
|
-
canonicalUri,
|
|
161
|
-
headers,
|
|
162
|
-
payloadHash,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const stringToSign = buildStringToSign({
|
|
166
|
-
amzDate,
|
|
167
|
-
dateStamp,
|
|
168
|
-
region: input.region,
|
|
169
|
-
service,
|
|
170
|
-
canonicalRequest,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const signingKey = deriveSigningKey(
|
|
174
|
-
input.credentials.secretAccessKey,
|
|
175
|
-
dateStamp,
|
|
176
|
-
input.region,
|
|
177
|
-
service,
|
|
178
|
-
);
|
|
179
|
-
const signature = hmac("sha256", signingKey, stringToSign).toString("hex");
|
|
180
|
-
const credentialScope = `${dateStamp}/${input.region}/${service}/aws4_request`;
|
|
181
|
-
|
|
182
|
-
headers.authorization =
|
|
183
|
-
`AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${credentialScope}, ` +
|
|
184
|
-
`SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
185
|
-
|
|
186
|
-
const base = usePathStyle
|
|
187
|
-
? String(input.endpoint).replace(/\/+$/, "")
|
|
188
|
-
: `https://${host}`;
|
|
189
|
-
const url = usePathStyle
|
|
190
|
-
? `${base}/${encodeS3Path(input.bucket)}/${keyPath}`
|
|
191
|
-
: `${base}/${keyPath}`;
|
|
192
|
-
|
|
193
|
-
return { url, headers, body };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* @param {PutObjectInput} input
|
|
198
|
-
* @returns {Promise<void>}
|
|
199
|
-
*/
|
|
200
|
-
export async function putObject(input) {
|
|
201
|
-
const signed = buildSignedPutObject(input);
|
|
202
|
-
const response = await fetch(signed.url, {
|
|
203
|
-
method: "PUT",
|
|
204
|
-
headers: signed.headers,
|
|
205
|
-
body: signed.body,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
if (!response.ok) {
|
|
209
|
-
const text = await response.text().catch(() => "");
|
|
210
|
-
throw new Error(
|
|
211
|
-
`S3 PutObject ${response.status}: ${text.slice(0, 200) || response.statusText}`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Minimal S3 PutObject — yalnızca SigV4 + `fetch`.
|
|
3
|
+
*
|
|
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
|
|
6
|
+
* (`JSKELET_S3_ACCESS_KEY_ID` / `JSKELET_S3_SECRET_ACCESS_KEY` /
|
|
7
|
+
* `JSKELET_S3_SESSION_TOKEN`); IAM role / instance metadata yok —
|
|
8
|
+
* container'a key vermeyen kurulumlar için sink zaten açılışta no-op'a düşer.
|
|
9
|
+
*/
|
|
10
|
+
import crypto from "node:crypto";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{ accessKeyId: string, secretAccessKey: string,
|
|
14
|
+
* sessionToken?: string | null }} AwsCredentials
|
|
15
|
+
*
|
|
16
|
+
* @typedef {{ bucket: string, key: string, body: string | Buffer,
|
|
17
|
+
* region: string, endpoint?: string | null, credentials: AwsCredentials,
|
|
18
|
+
* now?: Date }} PutObjectInput
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} algorithm
|
|
23
|
+
* @param {string | Buffer} key
|
|
24
|
+
* @param {string | Buffer} data
|
|
25
|
+
* @returns {Buffer}
|
|
26
|
+
*/
|
|
27
|
+
function hmac(algorithm, key, data) {
|
|
28
|
+
return crypto.createHmac(algorithm, key).update(data).digest();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string | Buffer} data
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function sha256Hex(data) {
|
|
36
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Date} date
|
|
41
|
+
* @returns {{ amzDate: string, dateStamp: string }}
|
|
42
|
+
*/
|
|
43
|
+
export function formatAmzDates(date) {
|
|
44
|
+
const iso = date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
45
|
+
return {
|
|
46
|
+
amzDate: iso,
|
|
47
|
+
dateStamp: iso.slice(0, 8),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} secretAccessKey
|
|
53
|
+
* @param {string} dateStamp
|
|
54
|
+
* @param {string} region
|
|
55
|
+
* @param {string} service
|
|
56
|
+
* @returns {Buffer}
|
|
57
|
+
*/
|
|
58
|
+
export function deriveSigningKey(secretAccessKey, dateStamp, region, service) {
|
|
59
|
+
const kDate = hmac("sha256", `AWS4${secretAccessKey}`, dateStamp);
|
|
60
|
+
const kRegion = hmac("sha256", kDate, region);
|
|
61
|
+
const kService = hmac("sha256", kRegion, service);
|
|
62
|
+
return hmac("sha256", kService, "aws4_request");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {{ method: string, canonicalUri: string, canonicalQuerystring?: string,
|
|
67
|
+
* headers: Record<string, string>, payloadHash: string }} parts
|
|
68
|
+
* @returns {{ canonicalRequest: string, signedHeaders: string }}
|
|
69
|
+
*/
|
|
70
|
+
export function buildCanonicalRequest(parts) {
|
|
71
|
+
const names = Object.keys(parts.headers)
|
|
72
|
+
.map((name) => name.toLowerCase())
|
|
73
|
+
.sort();
|
|
74
|
+
const canonicalHeaders = names
|
|
75
|
+
.map((name) => `${name}:${parts.headers[name].trim().replace(/\s+/g, " ")}\n`)
|
|
76
|
+
.join("");
|
|
77
|
+
const signedHeaders = names.join(";");
|
|
78
|
+
|
|
79
|
+
const canonicalRequest = [
|
|
80
|
+
parts.method,
|
|
81
|
+
parts.canonicalUri,
|
|
82
|
+
parts.canonicalQuerystring ?? "",
|
|
83
|
+
canonicalHeaders,
|
|
84
|
+
signedHeaders,
|
|
85
|
+
parts.payloadHash,
|
|
86
|
+
].join("\n");
|
|
87
|
+
|
|
88
|
+
return { canonicalRequest, signedHeaders };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {{ amzDate: string, dateStamp: string, region: string, service: string,
|
|
93
|
+
* canonicalRequest: string }} parts
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
export function buildStringToSign(parts) {
|
|
97
|
+
const scope = `${parts.dateStamp}/${parts.region}/${parts.service}/aws4_request`;
|
|
98
|
+
return [
|
|
99
|
+
"AWS4-HMAC-SHA256",
|
|
100
|
+
parts.amzDate,
|
|
101
|
+
scope,
|
|
102
|
+
sha256Hex(parts.canonicalRequest),
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* URI path encode (S3 key segment'leri). `/` korunur.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} value
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
export function encodeS3Path(value) {
|
|
113
|
+
return value
|
|
114
|
+
.split("/")
|
|
115
|
+
.map((segment) => encodeURIComponent(segment).replace(/[!'()*]/g, (char) =>
|
|
116
|
+
`%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
117
|
+
))
|
|
118
|
+
.join("/");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* PutObject isteğini imzalar; ağ çağrısı yapmaz — test edilebilir olsun diye.
|
|
123
|
+
*
|
|
124
|
+
* @param {PutObjectInput} input
|
|
125
|
+
* @returns {{ url: string, headers: Record<string, string>, body: Buffer }}
|
|
126
|
+
*/
|
|
127
|
+
export function buildSignedPutObject(input) {
|
|
128
|
+
const body = Buffer.isBuffer(input.body) ? input.body : Buffer.from(input.body);
|
|
129
|
+
const now = input.now ?? new Date();
|
|
130
|
+
const { amzDate, dateStamp } = formatAmzDates(now);
|
|
131
|
+
const service = "s3";
|
|
132
|
+
const payloadHash = sha256Hex(body);
|
|
133
|
+
|
|
134
|
+
const keyPath = encodeS3Path(input.key.replace(/^\/+/, ""));
|
|
135
|
+
const usePathStyle = Boolean(input.endpoint);
|
|
136
|
+
const endpointHost = input.endpoint
|
|
137
|
+
? new URL(input.endpoint).host
|
|
138
|
+
: `${input.bucket}.s3.${input.region}.amazonaws.com`;
|
|
139
|
+
const host = usePathStyle ? new URL(input.endpoint ?? "").host : endpointHost;
|
|
140
|
+
|
|
141
|
+
const canonicalUri = usePathStyle
|
|
142
|
+
? `/${encodeS3Path(input.bucket)}/${keyPath}`
|
|
143
|
+
: `/${keyPath}`;
|
|
144
|
+
|
|
145
|
+
/** @type {Record<string, string>} */
|
|
146
|
+
const headers = {
|
|
147
|
+
host,
|
|
148
|
+
"content-type": "application/x-ndjson",
|
|
149
|
+
"content-length": String(body.length),
|
|
150
|
+
"x-amz-content-sha256": payloadHash,
|
|
151
|
+
"x-amz-date": amzDate,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (input.credentials.sessionToken) {
|
|
155
|
+
headers["x-amz-security-token"] = input.credentials.sessionToken;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { canonicalRequest, signedHeaders } = buildCanonicalRequest({
|
|
159
|
+
method: "PUT",
|
|
160
|
+
canonicalUri,
|
|
161
|
+
headers,
|
|
162
|
+
payloadHash,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const stringToSign = buildStringToSign({
|
|
166
|
+
amzDate,
|
|
167
|
+
dateStamp,
|
|
168
|
+
region: input.region,
|
|
169
|
+
service,
|
|
170
|
+
canonicalRequest,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const signingKey = deriveSigningKey(
|
|
174
|
+
input.credentials.secretAccessKey,
|
|
175
|
+
dateStamp,
|
|
176
|
+
input.region,
|
|
177
|
+
service,
|
|
178
|
+
);
|
|
179
|
+
const signature = hmac("sha256", signingKey, stringToSign).toString("hex");
|
|
180
|
+
const credentialScope = `${dateStamp}/${input.region}/${service}/aws4_request`;
|
|
181
|
+
|
|
182
|
+
headers.authorization =
|
|
183
|
+
`AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${credentialScope}, ` +
|
|
184
|
+
`SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
185
|
+
|
|
186
|
+
const base = usePathStyle
|
|
187
|
+
? String(input.endpoint).replace(/\/+$/, "")
|
|
188
|
+
: `https://${host}`;
|
|
189
|
+
const url = usePathStyle
|
|
190
|
+
? `${base}/${encodeS3Path(input.bucket)}/${keyPath}`
|
|
191
|
+
: `${base}/${keyPath}`;
|
|
192
|
+
|
|
193
|
+
return { url, headers, body };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {PutObjectInput} input
|
|
198
|
+
* @returns {Promise<void>}
|
|
199
|
+
*/
|
|
200
|
+
export async function putObject(input) {
|
|
201
|
+
const signed = buildSignedPutObject(input);
|
|
202
|
+
const response = await fetch(signed.url, {
|
|
203
|
+
method: "PUT",
|
|
204
|
+
headers: signed.headers,
|
|
205
|
+
body: signed.body,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
const text = await response.text().catch(() => "");
|
|
210
|
+
throw new Error(
|
|
211
|
+
`S3 PutObject ${response.status}: ${text.slice(0, 200) || response.statusText}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S3 batch sink — satırları biriktirir, aralık / maxBatch ile PutObject atar.
|
|
3
|
-
*
|
|
4
|
-
* Başarısız bir yükleme süreci düşürmez: bir kez uyarır, batch'i bırakır.
|
|
5
|
-
* Backpressure için yeniden kuyruk yok — log kaybı, belleğin şişmesinden
|
|
6
|
-
* tercih edilir.
|
|
7
|
-
*/
|
|
8
|
-
import os from "node:os";
|
|
9
|
-
import process from "node:process";
|
|
10
|
-
import { putObject } from "./s3-put.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @typedef {import('./file-sink.js').LogSink} LogSink
|
|
14
|
-
* @typedef {import('./s3-put.js').AwsCredentials} AwsCredentials
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @param {{ bucket: string, prefix: string, region: string,
|
|
19
|
-
* endpoint?: string | null, credentials: AwsCredentials,
|
|
20
|
-
* flushIntervalMs: number, maxBatch: number,
|
|
21
|
-
* put?: typeof putObject }} options
|
|
22
|
-
* @returns {LogSink & { pendingCount: () => number }}
|
|
23
|
-
*/
|
|
24
|
-
export function createS3Sink(options) {
|
|
25
|
-
/** @type {string[]} */
|
|
26
|
-
let buffer = [];
|
|
27
|
-
let seq = 0;
|
|
28
|
-
let warned = false;
|
|
29
|
-
/** @type {Promise<void>} */
|
|
30
|
-
let chain = Promise.resolve();
|
|
31
|
-
/** @type {ReturnType<typeof setInterval> | null} */
|
|
32
|
-
let timer = null;
|
|
33
|
-
|
|
34
|
-
const put = options.put ?? putObject;
|
|
35
|
-
const host = os.hostname().replace(/[^a-zA-Z0-9._-]/g, "-") || "host";
|
|
36
|
-
const prefix = options.prefix.endsWith("/")
|
|
37
|
-
? options.prefix
|
|
38
|
-
: `${options.prefix}/`;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {Date} [date]
|
|
42
|
-
* @returns {string}
|
|
43
|
-
*/
|
|
44
|
-
function objectKey(date = new Date()) {
|
|
45
|
-
const y = date.getUTCFullYear();
|
|
46
|
-
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
47
|
-
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
48
|
-
const iso = date.toISOString().replace(/[:.]/g, "-");
|
|
49
|
-
seq += 1;
|
|
50
|
-
return `${prefix}${y}/${m}/${d}/${host}-${process.pid}-${iso}-${seq}.ndjson`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @param {string} message
|
|
55
|
-
* @param {unknown} [error]
|
|
56
|
-
*/
|
|
57
|
-
function warnOnce(message, error) {
|
|
58
|
-
if (warned) return;
|
|
59
|
-
warned = true;
|
|
60
|
-
console.warn(`[logs] ${message}`, error ?? "");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function flushNow() {
|
|
64
|
-
if (!buffer.length) return;
|
|
65
|
-
const lines = buffer;
|
|
66
|
-
buffer = [];
|
|
67
|
-
const body = `${lines.join("\n")}\n`;
|
|
68
|
-
try {
|
|
69
|
-
await put({
|
|
70
|
-
bucket: options.bucket,
|
|
71
|
-
key: objectKey(),
|
|
72
|
-
body,
|
|
73
|
-
region: options.region,
|
|
74
|
-
endpoint: options.endpoint ?? null,
|
|
75
|
-
credentials: options.credentials,
|
|
76
|
-
});
|
|
77
|
-
} catch (error) {
|
|
78
|
-
warnOnce("S3 PutObject failed; dropping batch", error);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Flush'ları sıraya dizer — iki timer çakışmasın. */
|
|
83
|
-
function enqueueFlush() {
|
|
84
|
-
chain = chain.then(flushNow).catch(() => {});
|
|
85
|
-
return chain;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
timer = setInterval(() => {
|
|
89
|
-
void enqueueFlush();
|
|
90
|
-
}, options.flushIntervalMs);
|
|
91
|
-
timer.unref?.();
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
pendingCount() {
|
|
95
|
-
return buffer.length;
|
|
96
|
-
},
|
|
97
|
-
async write(entry) {
|
|
98
|
-
buffer.push(JSON.stringify(entry));
|
|
99
|
-
if (buffer.length >= options.maxBatch) await enqueueFlush();
|
|
100
|
-
},
|
|
101
|
-
async flush() {
|
|
102
|
-
await enqueueFlush();
|
|
103
|
-
},
|
|
104
|
-
async close() {
|
|
105
|
-
if (timer) {
|
|
106
|
-
clearInterval(timer);
|
|
107
|
-
timer = null;
|
|
108
|
-
}
|
|
109
|
-
await enqueueFlush();
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* S3 batch sink — satırları biriktirir, aralık / maxBatch ile PutObject atar.
|
|
3
|
+
*
|
|
4
|
+
* Başarısız bir yükleme süreci düşürmez: bir kez uyarır, batch'i bırakır.
|
|
5
|
+
* Backpressure için yeniden kuyruk yok — log kaybı, belleğin şişmesinden
|
|
6
|
+
* tercih edilir.
|
|
7
|
+
*/
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
import { putObject } from "./s3-put.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {import('./file-sink.js').LogSink} LogSink
|
|
14
|
+
* @typedef {import('./s3-put.js').AwsCredentials} AwsCredentials
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {{ bucket: string, prefix: string, region: string,
|
|
19
|
+
* endpoint?: string | null, credentials: AwsCredentials,
|
|
20
|
+
* flushIntervalMs: number, maxBatch: number,
|
|
21
|
+
* put?: typeof putObject }} options
|
|
22
|
+
* @returns {LogSink & { pendingCount: () => number }}
|
|
23
|
+
*/
|
|
24
|
+
export function createS3Sink(options) {
|
|
25
|
+
/** @type {string[]} */
|
|
26
|
+
let buffer = [];
|
|
27
|
+
let seq = 0;
|
|
28
|
+
let warned = false;
|
|
29
|
+
/** @type {Promise<void>} */
|
|
30
|
+
let chain = Promise.resolve();
|
|
31
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
32
|
+
let timer = null;
|
|
33
|
+
|
|
34
|
+
const put = options.put ?? putObject;
|
|
35
|
+
const host = os.hostname().replace(/[^a-zA-Z0-9._-]/g, "-") || "host";
|
|
36
|
+
const prefix = options.prefix.endsWith("/")
|
|
37
|
+
? options.prefix
|
|
38
|
+
: `${options.prefix}/`;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {Date} [date]
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function objectKey(date = new Date()) {
|
|
45
|
+
const y = date.getUTCFullYear();
|
|
46
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
47
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
48
|
+
const iso = date.toISOString().replace(/[:.]/g, "-");
|
|
49
|
+
seq += 1;
|
|
50
|
+
return `${prefix}${y}/${m}/${d}/${host}-${process.pid}-${iso}-${seq}.ndjson`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} message
|
|
55
|
+
* @param {unknown} [error]
|
|
56
|
+
*/
|
|
57
|
+
function warnOnce(message, error) {
|
|
58
|
+
if (warned) return;
|
|
59
|
+
warned = true;
|
|
60
|
+
console.warn(`[logs] ${message}`, error ?? "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function flushNow() {
|
|
64
|
+
if (!buffer.length) return;
|
|
65
|
+
const lines = buffer;
|
|
66
|
+
buffer = [];
|
|
67
|
+
const body = `${lines.join("\n")}\n`;
|
|
68
|
+
try {
|
|
69
|
+
await put({
|
|
70
|
+
bucket: options.bucket,
|
|
71
|
+
key: objectKey(),
|
|
72
|
+
body,
|
|
73
|
+
region: options.region,
|
|
74
|
+
endpoint: options.endpoint ?? null,
|
|
75
|
+
credentials: options.credentials,
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
warnOnce("S3 PutObject failed; dropping batch", error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Flush'ları sıraya dizer — iki timer çakışmasın. */
|
|
83
|
+
function enqueueFlush() {
|
|
84
|
+
chain = chain.then(flushNow).catch(() => {});
|
|
85
|
+
return chain;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
timer = setInterval(() => {
|
|
89
|
+
void enqueueFlush();
|
|
90
|
+
}, options.flushIntervalMs);
|
|
91
|
+
timer.unref?.();
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
pendingCount() {
|
|
95
|
+
return buffer.length;
|
|
96
|
+
},
|
|
97
|
+
async write(entry) {
|
|
98
|
+
buffer.push(JSON.stringify(entry));
|
|
99
|
+
if (buffer.length >= options.maxBatch) await enqueueFlush();
|
|
100
|
+
},
|
|
101
|
+
async flush() {
|
|
102
|
+
await enqueueFlush();
|
|
103
|
+
},
|
|
104
|
+
async close() {
|
|
105
|
+
if (timer) {
|
|
106
|
+
clearInterval(timer);
|
|
107
|
+
timer = null;
|
|
108
|
+
}
|
|
109
|
+
await enqueueFlush();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|