jskelet 0.6.15 → 0.6.16

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
@@ -17,6 +17,12 @@ one is listed under a **Breaking** heading.
17
17
 
18
18
  ### Added
19
19
 
20
+ - When `build.assets.prefix` is set, the CSS build rewrites root-absolute
21
+ `url()` paths under `static.prefixes` (e.g. `/fonts/…`) to the CDN
22
+ prefix so they match the S3 key (`…/js/fonts/…`). Default upload
23
+ `include` is `["assets", "fonts"]`; prefix also forces `fonts` into
24
+ `include`. Font uploads run PutBucketCors with
25
+ `upload.corsOrigin` (default `"*"`, set `null` to skip).
20
26
  - `asset(name, { origin: "document" })` skips `build.assets.prefix` and
21
27
  returns the site-origin `/assets/…` path. `icon()` always resolves the
22
28
  sprite this way so SVG `<use href>` keeps working when other assets are
@@ -635,7 +635,8 @@ değişmez** — asset'ler origin'den servis edilir.
635
635
  | `assets.upload.keyPrefix` | `string` | `""` | Nesne anahtarı öneki; bucket yolunda verilmişse o kazanır (`prefix` takma adı da kabul). |
636
636
  | `assets.upload.region` | `string` | `us-east-1` | Config; custom `endpoint` varken varsayılan `auto`. |
637
637
  | `assets.upload.endpoint` | `string \| null` | `null` | S3-uyumlu API (R2 / MinIO); config. |
638
- | `assets.upload.include` | `string[]` | `["assets"]` | `public/` altı dizinler (`assets`, isteğe `fonts`, …). |
638
+ | `assets.upload.include` | `string[]` | `["assets", "fonts"]` | `public/` altı dizinler. `assets.prefix` doluysa `fonts` yoksa eklenir. |
639
+ | `assets.upload.corsOrigin` | `string \| null` | `"*"` | Font upload'ta PutBucketCors `AllowedOrigin`; `null` kapatır (önceki CORS'un üzerine yazar). |
639
640
  | `assets.upload.cacheControl` | `string` | `public, max-age=31536000, immutable` | PutObject `Cache-Control`. |
640
641
 
641
642
  **Credential config'e yazılmaz.** Zincir: `AWS_ACCESS_KEY_ID` /
package/docs/08-build.md CHANGED
@@ -91,12 +91,21 @@ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
91
91
  Varsayılan kapalıdır. Prefix / bucket / keyPrefix config'te; credential
92
92
  env → ECS task role → EC2 IMDS zincirinden. Açılınca:
93
93
 
94
- 1. Production build `public/assets/` (ve `include` listesindeki diğer dizinler)
95
- altındaki dosyaları S3'e PutObject ile kopyalar.
96
- 2. Çalışma anında `asset()` / optimize görseller `build.assets.prefix` ile
97
- absolute CDN URL basar. Entry CDN'deyse esbuild chunk'ları göreli yolla aynı
98
- host'a gider. `icon()` sprite'ı ve `asset(name, { origin: "document" })`
99
- site origin'inde kalır.
94
+ 1. Production build `public/assets/` ve `public/fonts/` (varsayılan `include`)
95
+ altındaki dosyaları S3'e PutObject ile kopyalar. `assets.prefix` doluysa
96
+ `include`'da `fonts` yoksa otomatik eklenir.
97
+ 2. CSS derlemesi `static.prefixes` kök-mutlak `url()` yollarını
98
+ (`/fonts/…`, `/assets/…`) `prefix` ile yeniden yazar — aksi hâlde CDN'deki
99
+ stylesheet `url("/fonts/x")` yolunu host köküne çözer ve `keyPrefix`
100
+ kaybolur. `@font-face` CDN CSS içinde kalabilir; font ile CSS aynı host'ta
101
+ olduğu için ek CORS gerekmez.
102
+ 3. Font yüklendiğinde ve `upload.corsOrigin` doluysa (varsayılan `"*"`)
103
+ PutBucketCors bir GET/HEAD kuralı yazar — bucket'taki önceki CORS'un
104
+ **üzerine** yazar; kapatmak için `corsOrigin: null`. HTML'den çapraz
105
+ origin font için bu (veya CloudFront response header) gerekir.
106
+ 4. Çalışma anında `asset()` / optimize görseller `build.assets.prefix` ile
107
+ absolute CDN URL basar. `icon()` sprite'ı ve
108
+ `asset(name, { origin: "document" })` site origin'inde kalır.
100
109
 
101
110
  Bucket adı noktalı olabilir (`static.ornek.com`); `https://…` şeması
102
111
  `prefix`'e yazılır. Config sözlüğü: [07-yapilandirma.md](./07-yapilandirma.md#build).
@@ -297,6 +306,10 @@ Kullanımı stylesheet'te elle yazılır:
297
306
  `.woff2` uzantısı ve `/fonts/` öneki varsayılan `static` kurallarında olduğu için
298
307
  bu dosyalara otomatik olarak `immutable` cache yazılır.
299
308
 
309
+ `build.assets.prefix` açıksa build, CSS'teki `url("/fonts/…")` yollarını CDN
310
+ prefix'iyle yeniden yazar ve fontları S3'e de basar — layout'ta ayrı
311
+ `@font-face` tutmaya gerek kalmaz.
312
+
300
313
  ## İkon sprite
301
314
 
302
315
  **Yalnızca kaynakta kullanılan** ikonlar için bir `<symbol>` seti üretir. Tüm
@@ -648,7 +648,8 @@ changes** — assets stay on the origin.
648
648
  | `assets.upload.keyPrefix` | `string` | `""` | Object key prefix; a path on the bucket wins (`prefix` is accepted as an alias). |
649
649
  | `assets.upload.region` | `string` | `us-east-1` | Config; defaults to `auto` when `endpoint` is set. |
650
650
  | `assets.upload.endpoint` | `string \| null` | `null` | S3-compatible API (R2 / MinIO); config. |
651
- | `assets.upload.include` | `string[]` | `["assets"]` | Directories under `public/` (`assets`, optionally `fonts`, …). |
651
+ | `assets.upload.include` | `string[]` | `["assets", "fonts"]` | Directories under `public/`. When `assets.prefix` is set, `fonts` is added if missing. |
652
+ | `assets.upload.corsOrigin` | `string \| null` | `"*"` | PutBucketCors `AllowedOrigin` when fonts upload; `null` disables (replaces prior bucket CORS). |
652
653
  | `assets.upload.cacheControl` | `string` | `public, max-age=31536000, immutable` | PutObject `Cache-Control`. |
653
654
 
654
655
  **Credentials are not written in the config.** Chain: `AWS_ACCESS_KEY_ID` /
@@ -94,11 +94,20 @@ They are passed to templates automatically; in server code,
94
94
  Off by default. Prefix / bucket / keyPrefix come from config; credentials
95
95
  resolve from env → ECS task role → EC2 IMDS. When enabled:
96
96
 
97
- 1. A production build PutObjects files under `public/assets/` (and any other
98
- directories in `include`) to S3.
99
- 2. At runtime `asset()` / optimised images emit absolute CDN URLs via
100
- `build.assets.prefix`. When the entry script is on the CDN, esbuild chunks
101
- resolve relatively on the same host. The `icon()` sprite and
97
+ 1. A production build PutObjects `public/assets/` and `public/fonts/`
98
+ (default `include`) to S3. When `assets.prefix` is set, `fonts` is added
99
+ to `include` if missing.
100
+ 2. The CSS step rewrites root-absolute `url()` paths under `static.prefixes`
101
+ (`/fonts/…`, `/assets/…`) with `prefix` — otherwise a stylesheet on the CDN
102
+ resolves `url("/fonts/x")` against the host root and drops `keyPrefix`.
103
+ `@font-face` can stay in the CDN CSS; font and CSS share a host, so no
104
+ extra CORS is needed for that path.
105
+ 3. When fonts are uploaded and `upload.corsOrigin` is set (default `"*"`),
106
+ PutBucketCors writes a GET/HEAD rule — it **replaces** the bucket's
107
+ previous CORS config; set `corsOrigin: null` to skip. Cross-origin fonts
108
+ from HTML still need this (or a CloudFront response-header policy).
109
+ 4. At runtime `asset()` / optimised images emit absolute CDN URLs via
110
+ `build.assets.prefix`. The `icon()` sprite and
102
111
  `asset(name, { origin: "document" })` stay on the document origin.
103
112
 
104
113
  Bucket names may include dots (`static.example.com`); put `https://…` in
@@ -305,6 +314,10 @@ Usage is written by hand in the stylesheet:
305
314
  Because the `.woff2` extension and the `/fonts/` prefix are in the default
306
315
  `static` rules, these files automatically get an `immutable` cache.
307
316
 
317
+ When `build.assets.prefix` is set, the build rewrites `url("/fonts/…")` in CSS
318
+ to the CDN prefix and uploads the font files too — you do not need a separate
319
+ `@font-face` block in the layout.
320
+
308
321
  ## Icon sprite
309
322
 
310
323
  Produces a `<symbol>` set for **only the icons actually used in the source**.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.6.15",
3
+ "version": "0.6.16",
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",
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CDN prefix varken CSS `url(/fonts/…)` yollarını upload path'iyle hizalar.
3
+ *
4
+ * CDN'deki `app.css` kök-mutlak `/fonts/x.woff2` görünce tarayıcı bunu
5
+ * `https://cdn.example.com/fonts/x` diye çözer — `keyPrefix` (`/js` vb.)
6
+ * kaybolur ve 403 gelir. Prefix + aynı göreli yol (`…/js/fonts/x`) doğru
7
+ * nesneye gider; font ile CSS aynı host'ta olduğu için `@font-face` CORS
8
+ * istemez. HTML'den CDN font yüklemek ayrı: bucket/CloudFront CORS gerekir.
9
+ */
10
+ import { applyAssetPrefix } from "../server/assets.js";
11
+
12
+ /**
13
+ * @param {string} css
14
+ * @param {{ prefix: string, prefixes?: string[] }} options
15
+ * @returns {string}
16
+ */
17
+ export function rewriteCssStaticUrls(css, { prefix, prefixes = [] }) {
18
+ if (!prefix || !css) return css;
19
+
20
+ const roots = [...prefixes]
21
+ .filter((entry) => typeof entry === "string" && entry.startsWith("/"))
22
+ .map((entry) => (entry.endsWith("/") ? entry : `${entry}/`))
23
+ .sort((a, b) => b.length - a.length);
24
+
25
+ if (!roots.length) return css;
26
+
27
+ return css.replace(
28
+ /url\(\s*(['"]?)(\/[^)"'\s]+)\1\s*\)/gi,
29
+ (match, quote, urlPath) => {
30
+ const pathOnly = urlPath.split(/[?#]/, 1)[0];
31
+ if (!roots.some((root) => pathOnly === root.slice(0, -1) || pathOnly.startsWith(root))) {
32
+ return match;
33
+ }
34
+
35
+ const rewritten = applyAssetPrefix(prefix, urlPath);
36
+ const q = quote || '"';
37
+ return `url(${q}${rewritten}${q})`;
38
+ },
39
+ );
40
+ }
@@ -16,12 +16,22 @@ import {
16
16
  resolveS3Credentials,
17
17
  s3CredentialsMissingReason,
18
18
  } from "../../server/logs/s3-credentials.js";
19
- import { putObject } from "../../server/logs/s3-put.js";
19
+ import { putBucketCors, putObject } from "../../server/logs/s3-put.js";
20
20
  import * as log from "../../log.mjs";
21
21
  import { paths } from "../paths.mjs";
22
22
 
23
23
  const CONCURRENCY = 8;
24
24
 
25
+ /** Font MIME — CDN'den HTML çapraz origin yüklerken CORS gerekir. */
26
+ const FONT_MIME = new Set([
27
+ "font/woff",
28
+ "font/woff2",
29
+ "font/ttf",
30
+ "font/otf",
31
+ "application/font-woff",
32
+ "application/font-woff2",
33
+ ]);
34
+
25
35
  /** @type {Record<string, string>} */
26
36
  const MIME = {
27
37
  ".js": "application/javascript; charset=utf-8",
@@ -116,6 +126,7 @@ export function assetsUploadSkipReason(config) {
116
126
  /**
117
127
  * @param {import('../../config/index.js').ResolvedConfig} config
118
128
  * @param {{ put?: typeof putObject,
129
+ * putCors?: typeof putBucketCors,
119
130
  * resolveCredentials?: typeof resolveS3Credentials }} [options]
120
131
  * @returns {Promise<void>}
121
132
  */
@@ -135,6 +146,7 @@ export async function uploadAssets(config, options = {}) {
135
146
  }
136
147
 
137
148
  const put = options.put ?? putObject;
149
+ const putCors = options.putCors ?? putBucketCors;
138
150
  // Normalize bazen atlanır (test / elle config); önek `/` ile bitsin.
139
151
  let keyPrefix = (upload.keyPrefix || "").replace(/^\/+/, "");
140
152
  if (keyPrefix && !keyPrefix.endsWith("/")) keyPrefix += "/";
@@ -162,6 +174,27 @@ export async function uploadAssets(config, options = {}) {
162
174
  return;
163
175
  }
164
176
 
177
+ const uploadsFonts = jobs.some((job) =>
178
+ FONT_MIME.has(contentTypeFor(job.file)),
179
+ );
180
+ if (uploadsFonts && upload.corsOrigin) {
181
+ try {
182
+ await putCors({
183
+ bucket: upload.bucket,
184
+ region: upload.region,
185
+ endpoint: upload.endpoint,
186
+ credentials,
187
+ allowedOrigin: upload.corsOrigin,
188
+ });
189
+ log.detail(`cors ${upload.corsOrigin}`);
190
+ } catch (error) {
191
+ const message = error instanceof Error ? error.message : String(error);
192
+ console.warn(
193
+ `[assets] PutBucketCors failed (${message}) — set CDN/bucket CORS manually for cross-origin fonts`,
194
+ );
195
+ }
196
+ }
197
+
165
198
  let uploaded = 0;
166
199
  let index = 0;
167
200
 
@@ -24,6 +24,7 @@
24
24
  import fs from "node:fs";
25
25
  import path from "node:path";
26
26
  import { paths, writeAsset, writeManifest } from "../paths.mjs";
27
+ import { rewriteCssStaticUrls } from "../rewrite-css-urls.mjs";
27
28
  import { importFromApp, tryImportFromApp } from "../resolve-peer.mjs";
28
29
  import * as log from "../../log.mjs";
29
30
 
@@ -85,6 +86,23 @@ function listPageStyles(dir) {
85
86
  .map((name) => path.join(dir, name));
86
87
  }
87
88
 
89
+ /**
90
+ * `build.assets.prefix` doluysa `static.prefixes` kök-mutlak `url()` yollarını
91
+ * CDN'e çevirir (`/fonts/…` → `https://cdn…/js/fonts/…`).
92
+ *
93
+ * @param {string} css
94
+ * @param {import('../../config/index.js').ResolvedConfig} config
95
+ * @returns {string}
96
+ */
97
+ function applyCdnStaticUrls(css, config) {
98
+ const prefix = config.build?.assets?.prefix ?? "";
99
+ if (!prefix) return css;
100
+ return rewriteCssStaticUrls(css, {
101
+ prefix,
102
+ prefixes: config.static?.prefixes ?? ["/assets/", "/fonts/"],
103
+ });
104
+ }
105
+
88
106
  /**
89
107
  * @param {import('../../config/index.js').ResolvedConfig} config
90
108
  * @param {{ watch?: boolean }} [options]
@@ -101,7 +119,7 @@ export async function buildCss(config, { watch = false } = {}) {
101
119
  const urls = {};
102
120
  let bytes = 0;
103
121
 
104
- const appCss = await compile(input);
122
+ const appCss = applyCdnStaticUrls(await compile(input), config);
105
123
  const appUrl = writeAsset("app.css", appCss);
106
124
  urls["app.css"] = appUrl;
107
125
  bytes += Buffer.byteLength(appCss);
@@ -111,7 +129,7 @@ export async function buildCss(config, { watch = false } = {}) {
111
129
 
112
130
  for (const file of listPageStyles(pagesDir)) {
113
131
  const name = path.basename(file);
114
- const css = await compile(file);
132
+ const css = applyCdnStaticUrls(await compile(file), config);
115
133
  const url = writeAsset(name, css);
116
134
  urls[name] = url;
117
135
  bytes += Buffer.byteLength(css);
@@ -508,9 +508,19 @@ export const DEFAULT_BUILD = {
508
508
  keyPrefix: "",
509
509
  region: "us-east-1",
510
510
  endpoint: null,
511
- /** `public/` altı dizin adları. */
512
- include: /** @type {string[]} */ (["assets"]),
511
+ /**
512
+ * `public/` altı dizin adları. `fonts` varsayılan: CDN CSS'teki
513
+ * `/fonts/…` rewrite'ı upload nesnesiyle hizalansın.
514
+ */
515
+ include: /** @type {string[]} */ (["assets", "fonts"]),
513
516
  cacheControl: "public, max-age=31536000, immutable",
517
+ /**
518
+ * Upload sırasında PutBucketCors ile yazılan `AllowedOrigin`.
519
+ * `null` / `false` → CORS adımı atlanır. Bucket'taki önceki CORS
520
+ * kuralının üzerine yazar — paylaşımlı kovada bilinçli açın.
521
+ * CDN CSS + aynı host font için genelde gerekmez.
522
+ */
523
+ corsOrigin: /** @type {string | null} */ ("*"),
514
524
  },
515
525
  },
516
526
  };
@@ -184,6 +184,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
184
184
  * @property {string | null} endpoint
185
185
  * @property {string[]} include
186
186
  * @property {string} cacheControl
187
+ * @property {string | null} corsOrigin Font/CDN PutBucketCors `AllowedOrigin`; `null` kapatır.
187
188
  *
188
189
  * @typedef {object} BuildAssetsConfig
189
190
  * @property {string} prefix CDN origin (`build.assets.prefix`); sonunda `/` yok.
@@ -1264,11 +1265,25 @@ export function normalizeBuildAssets(raw) {
1264
1265
  }
1265
1266
  }
1266
1267
 
1268
+ // CDN CSS `/fonts/…` → prefix'li URL yazar; fontlar upload'ta yoksa 403.
1269
+ // Kullanıcı `include: ["assets"]` yazsa bile prefix varken `fonts` eklenir.
1270
+ if (prefix && !include.includes("fonts")) {
1271
+ include = [...include, "fonts"];
1272
+ }
1273
+
1267
1274
  const cacheControl =
1268
1275
  typeof uploadRaw.cacheControl === "string" && uploadRaw.cacheControl.trim()
1269
1276
  ? uploadRaw.cacheControl.trim()
1270
1277
  : DEFAULT_BUILD.assets.upload.cacheControl;
1271
1278
 
1279
+ /** @type {string | null} */
1280
+ let corsOrigin = DEFAULT_BUILD.assets.upload.corsOrigin;
1281
+ if (uploadRaw.corsOrigin === null || uploadRaw.corsOrigin === false) {
1282
+ corsOrigin = null;
1283
+ } else if (typeof uploadRaw.corsOrigin === "string") {
1284
+ corsOrigin = uploadRaw.corsOrigin.trim() || null;
1285
+ }
1286
+
1272
1287
  // Config'te bucket varsa `enabled` unutulmuş olsa bile aç. Açık `false` ezer.
1273
1288
  const enabled =
1274
1289
  uploadRaw.enabled === true ||
@@ -1284,6 +1299,7 @@ export function normalizeBuildAssets(raw) {
1284
1299
  endpoint,
1285
1300
  include,
1286
1301
  cacheControl,
1302
+ corsOrigin,
1287
1303
  },
1288
1304
  };
1289
1305
  }
@@ -14,6 +14,9 @@ import crypto from "node:crypto";
14
14
  * @typedef {{ bucket: string, key: string, body: string | Buffer,
15
15
  * region: string, endpoint?: string | null, credentials: AwsCredentials,
16
16
  * contentType?: string, cacheControl?: string | null, now?: Date }} PutObjectInput
17
+ *
18
+ * @typedef {{ bucket: string, region: string, endpoint?: string | null,
19
+ * credentials: AwsCredentials, allowedOrigin: string, now?: Date }} PutBucketCorsInput
17
20
  */
18
21
 
19
22
  /**
@@ -131,6 +134,24 @@ export function useS3PathStyle(bucket, endpoint) {
131
134
  return bucket.includes(".");
132
135
  }
133
136
 
137
+ /**
138
+ * @param {{ bucket: string, region: string, endpoint?: string | null }} input
139
+ * @returns {{ host: string, base: string, pathStyle: boolean }}
140
+ */
141
+ function s3EndpointParts(input) {
142
+ const pathStyle = useS3PathStyle(input.bucket, input.endpoint);
143
+ if (input.endpoint) {
144
+ const base = String(input.endpoint).replace(/\/+$/, "");
145
+ return { host: new URL(input.endpoint).host, base, pathStyle: true };
146
+ }
147
+ if (pathStyle) {
148
+ const host = `s3.${input.region}.amazonaws.com`;
149
+ return { host, base: `https://${host}`, pathStyle: true };
150
+ }
151
+ const host = `${input.bucket}.s3.${input.region}.amazonaws.com`;
152
+ return { host, base: `https://${host}`, pathStyle: false };
153
+ }
154
+
134
155
  /**
135
156
  * PutObject isteğini imzalar; ağ çağrısı yapmaz — test edilebilir olsun diye.
136
157
  *
@@ -145,22 +166,7 @@ export function buildSignedPutObject(input) {
145
166
  const payloadHash = sha256Hex(body);
146
167
 
147
168
  const keyPath = encodeS3Path(input.key.replace(/^\/+/, ""));
148
- const pathStyle = useS3PathStyle(input.bucket, input.endpoint);
149
-
150
- /** @type {string} */
151
- let host;
152
- /** @type {string} */
153
- let base;
154
- if (input.endpoint) {
155
- base = String(input.endpoint).replace(/\/+$/, "");
156
- host = new URL(input.endpoint).host;
157
- } else if (pathStyle) {
158
- host = `s3.${input.region}.amazonaws.com`;
159
- base = `https://${host}`;
160
- } else {
161
- host = `${input.bucket}.s3.${input.region}.amazonaws.com`;
162
- base = `https://${host}`;
163
- }
169
+ const { host, base, pathStyle } = s3EndpointParts(input);
164
170
 
165
171
  const canonicalUri = pathStyle
166
172
  ? `/${encodeS3Path(input.bucket)}/${keyPath}`
@@ -219,6 +225,132 @@ export function buildSignedPutObject(input) {
219
225
  return { url, headers, body };
220
226
  }
221
227
 
228
+ /**
229
+ * PutBucketCors gövdesi — tek kural, GET/HEAD.
230
+ *
231
+ * @param {string} allowedOrigin
232
+ * @returns {string}
233
+ */
234
+ export function corsConfigurationXml(allowedOrigin) {
235
+ const origin = String(allowedOrigin)
236
+ .replace(/&/g, "&amp;")
237
+ .replace(/</g, "&lt;")
238
+ .replace(/>/g, "&gt;")
239
+ .replace(/"/g, "&quot;");
240
+
241
+ return (
242
+ `<?xml version="1.0" encoding="UTF-8"?>` +
243
+ `<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">` +
244
+ `<CORSRule>` +
245
+ `<AllowedOrigin>${origin}</AllowedOrigin>` +
246
+ `<AllowedMethod>GET</AllowedMethod>` +
247
+ `<AllowedMethod>HEAD</AllowedMethod>` +
248
+ `<AllowedHeader>*</AllowedHeader>` +
249
+ `<ExposeHeader>ETag</ExposeHeader>` +
250
+ `<ExposeHeader>Content-Type</ExposeHeader>` +
251
+ `<MaxAgeSeconds>86400</MaxAgeSeconds>` +
252
+ `</CORSRule>` +
253
+ `</CORSConfiguration>`
254
+ );
255
+ }
256
+
257
+ /**
258
+ * PutBucketCors isteğini imzalar (`PUT /?cors` veya path-style `/bucket?cors`).
259
+ *
260
+ * @param {PutBucketCorsInput} input
261
+ * @returns {{ url: string, headers: Record<string, string>, body: Buffer }}
262
+ */
263
+ export function buildSignedPutBucketCors(input) {
264
+ const body = Buffer.from(corsConfigurationXml(input.allowedOrigin), "utf8");
265
+ const now = input.now ?? new Date();
266
+ const { amzDate, dateStamp } = formatAmzDates(now);
267
+ const payloadHash = sha256Hex(body);
268
+ const { host, base, pathStyle } = s3EndpointParts(input);
269
+
270
+ // AWS: cors alt kaynağının canonical query'si `cors=` (boş değer).
271
+ const canonicalQuerystring = "cors=";
272
+ const canonicalUri = pathStyle ? `/${encodeS3Path(input.bucket)}` : "/";
273
+
274
+ /** @type {Record<string, string>} */
275
+ const headers = {
276
+ host,
277
+ "content-type": "application/xml",
278
+ "content-length": String(body.length),
279
+ "x-amz-content-sha256": payloadHash,
280
+ "x-amz-date": amzDate,
281
+ };
282
+
283
+ if (input.credentials.sessionToken) {
284
+ headers["x-amz-security-token"] = input.credentials.sessionToken;
285
+ }
286
+
287
+ const { canonicalRequest, signedHeaders } = buildCanonicalRequest({
288
+ method: "PUT",
289
+ canonicalUri,
290
+ canonicalQuerystring,
291
+ headers,
292
+ payloadHash,
293
+ });
294
+
295
+ const stringToSign = buildStringToSign({
296
+ amzDate,
297
+ dateStamp,
298
+ region: input.region,
299
+ service: "s3",
300
+ canonicalRequest,
301
+ });
302
+
303
+ const signingKey = deriveSigningKey(
304
+ input.credentials.secretAccessKey,
305
+ dateStamp,
306
+ input.region,
307
+ "s3",
308
+ );
309
+ const signature = hmac("sha256", signingKey, stringToSign).toString("hex");
310
+ const credentialScope = `${dateStamp}/${input.region}/s3/aws4_request`;
311
+
312
+ headers.authorization =
313
+ `AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${credentialScope}, ` +
314
+ `SignedHeaders=${signedHeaders}, Signature=${signature}`;
315
+
316
+ const url = pathStyle
317
+ ? `${base}/${encodeS3Path(input.bucket)}?cors`
318
+ : `${base}/?cors`;
319
+
320
+ return { url, headers, body };
321
+ }
322
+
323
+ /**
324
+ * Bucket CORS yazar. Mevcut kuralların **üzerine** yazar; paylaşımlı
325
+ * kovada bilinçli kullanılmalı. İzin yoksa çağıran uyarır, upload sürer.
326
+ *
327
+ * @param {PutBucketCorsInput} input
328
+ * @returns {Promise<void>}
329
+ */
330
+ export async function putBucketCors(input) {
331
+ const signed = buildSignedPutBucketCors(input);
332
+ /** @type {Response} */
333
+ let response;
334
+ try {
335
+ response = await fetch(signed.url, {
336
+ method: "PUT",
337
+ headers: signed.headers,
338
+ body: signed.body,
339
+ });
340
+ } catch (error) {
341
+ const detail =
342
+ error instanceof Error ? error.message : String(error);
343
+ throw new Error(`S3 PutBucketCors fetch failed (${signed.url}): ${detail}`);
344
+ }
345
+
346
+ if (!response.ok) {
347
+ const text = await response.text().catch(() => "");
348
+ throw new Error(
349
+ `S3 PutBucketCors ${response.status}: ${text.slice(0, 200) || response.statusText}`,
350
+ );
351
+ }
352
+ }
353
+
222
354
  /**
223
355
  * @param {PutObjectInput} input
224
356
  * @returns {Promise<void>}
@@ -458,9 +458,19 @@ export declare const DEFAULT_BUILD: {
458
458
  keyPrefix: string;
459
459
  region: string;
460
460
  endpoint: null;
461
- /** `public/` altı dizin adları. */
461
+ /**
462
+ * `public/` altı dizin adları. `fonts` varsayılan: CDN CSS'teki
463
+ * `/fonts/…` rewrite'ı upload nesnesiyle hizalansın.
464
+ */
462
465
  include: string[];
463
466
  cacheControl: string;
467
+ /**
468
+ * Upload sırasında PutBucketCors ile yazılan `AllowedOrigin`.
469
+ * `null` / `false` → CORS adımı atlanır. Bucket'taki önceki CORS
470
+ * kuralının üzerine yazar — paylaşımlı kovada bilinçli açın.
471
+ * CDN CSS + aynı host font için genelde gerekmez.
472
+ */
473
+ corsOrigin: string | null;
464
474
  };
465
475
  };
466
476
  };
@@ -240,6 +240,10 @@ export type BuildAssetsUploadConfig = {
240
240
  endpoint: string | null;
241
241
  include: string[];
242
242
  cacheControl: string;
243
+ /**
244
+ * Font/CDN PutBucketCors `AllowedOrigin`; `null` kapatır.
245
+ */
246
+ corsOrigin: string | null;
243
247
  };
244
248
  export type BuildAssetsConfig = {
245
249
  /**
@@ -14,6 +14,14 @@ export type PutObjectInput = {
14
14
  cacheControl?: string | null;
15
15
  now?: Date;
16
16
  };
17
+ export type PutBucketCorsInput = {
18
+ bucket: string;
19
+ region: string;
20
+ endpoint?: string | null;
21
+ credentials: AwsCredentials;
22
+ allowedOrigin: string;
23
+ now?: Date;
24
+ };
17
25
  /**
18
26
  * @param {string | Buffer} data
19
27
  * @returns {string}
@@ -91,6 +99,32 @@ export declare function buildSignedPutObject(input: PutObjectInput): {
91
99
  headers: Record<string, string>;
92
100
  body: Buffer;
93
101
  };
102
+ /**
103
+ * PutBucketCors gövdesi — tek kural, GET/HEAD.
104
+ *
105
+ * @param {string} allowedOrigin
106
+ * @returns {string}
107
+ */
108
+ export declare function corsConfigurationXml(allowedOrigin: string): string;
109
+ /**
110
+ * PutBucketCors isteğini imzalar (`PUT /?cors` veya path-style `/bucket?cors`).
111
+ *
112
+ * @param {PutBucketCorsInput} input
113
+ * @returns {{ url: string, headers: Record<string, string>, body: Buffer }}
114
+ */
115
+ export declare function buildSignedPutBucketCors(input: PutBucketCorsInput): {
116
+ url: string;
117
+ headers: Record<string, string>;
118
+ body: Buffer;
119
+ };
120
+ /**
121
+ * Bucket CORS yazar. Mevcut kuralların **üzerine** yazar; paylaşımlı
122
+ * kovada bilinçli kullanılmalı. İzin yoksa çağıran uyarır, upload sürer.
123
+ *
124
+ * @param {PutBucketCorsInput} input
125
+ * @returns {Promise<void>}
126
+ */
127
+ export declare function putBucketCors(input: PutBucketCorsInput): Promise<void>;
94
128
  /**
95
129
  * @param {PutObjectInput} input
96
130
  * @returns {Promise<void>}