@ryusenkei/maezae 0.17.0 → 0.18.1

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/README.md CHANGED
@@ -181,6 +181,24 @@ const html = bodyToHtml(entry.body, {
181
181
 
182
182
  章の分割などで見出しの文字を使うときは、`plainOf(heading.content)` で文字にする(取り込みは見出しの文字を変えない)
183
183
 
184
+ ## 非公開のメディア(0.18.0 から)
185
+
186
+ 非公開の項目(型の宣言で `visibility: 'private'` のメディアと本文)の画像とファイルは、署名つき・期限つきの URL でだけ配信する。配信 API は `visibility: 'private'` と署名のない `url` を返すので、権利のある人に見せるときにサイトのサーバーが署名する
187
+
188
+ - 鍵: 媒体の管理画面の「設定 → 連携」の非公開のメディアの署名の鍵。サイトの秘密値に入れ、ブラウザに出さない
189
+ - 期限: `expiresInSec`(既定は3600秒)。ページを表示するたびに署名し、写しに署名つきの URL を保存しない
190
+ - 画像の変換のクエリ(`w` など)は署名の後に足してよい(署名の対象はパスと期限だけ)
191
+ - 署名のない URL は404、期限切れと別の鍵の署名は403。鍵を作り直すと古い鍵の署名はすぐ通らない
192
+
193
+ ```ts
194
+ import { signMediaUrl, withImageParams } from '@ryusenkei/maezae/delivery';
195
+
196
+ const src = image.visibility === 'private' ? await signMediaUrl(image.url, env.MEDIA_SIGNING_KEY, { expiresInSec: 1800 }) : image.url;
197
+ const thumb = withImageParams(src, { w: 400, auto: 'format' });
198
+ ```
199
+
200
+ ファイル(`kind: 'file'`): `url` は `/files/<媒体>/<ID>.<拡張子>`、`name`(元のファイル名)・`mimeType`・`size` を持つ。本文のファイルのブロックも同じ実体を参照する
201
+
184
202
  ## プレビュー
185
203
 
186
204
  - CMS は媒体のサイトのオリジンへ、公開のページと同じパスで署名つきのリクエストを送信
package/dist/body.js CHANGED
@@ -72,6 +72,32 @@ const SAFE_HREF = /^(?:https?:\/\/|mailto:|#|\/(?![/\\]))/i;
72
72
  const isSafeHref = (href) => SAFE_HREF.test(href);
73
73
  /** 許可しないリンク先の問題の文言 */
74
74
  const unsafeHrefMessage = (href) => `リンク先 ${href} は使えない(https://・http://・mailto:・/ で始まるサイト内のパス・# で始まるページ内のリンクだけ)`;
75
+ // ../body/src/inline.ts
76
+ /** 装飾の順序(外側から内側、Markdown と HTML の入れ子の順) */
77
+ const MARK_ORDER = [
78
+ "strong",
79
+ "em",
80
+ "strike",
81
+ "underline",
82
+ "mark",
83
+ "sup",
84
+ "sub",
85
+ "code"
86
+ ];
87
+ /** 装飾を MARK_ORDER の順に並べる(重複は1つ) */
88
+ const sortMarks = (marks) => MARK_ORDER.filter((m) => marks?.includes(m));
89
+ /** 装飾の描画の HTML の要素 */
90
+ const MARK_HTML_TAGS = {
91
+ strong: "strong",
92
+ em: "em",
93
+ code: "code",
94
+ strike: "s",
95
+ mark: "mark",
96
+ sup: "sup",
97
+ sub: "sub",
98
+ underline: "u"
99
+ };
100
+ ({ ...Object.fromEntries(Object.entries(MARK_HTML_TAGS).map(([mark, tag]) => [tag, mark])) });
75
101
  // ../body/src/profile.ts
76
102
  const MARK_LABELS = {
77
103
  strong: "太字",
@@ -240,27 +266,6 @@ const ESCAPES = {
240
266
  };
241
267
  const esc = (s) => s.replace(/[&<>"']/g, (c) => ESCAPES[c]);
242
268
  const attrs = (pairs) => Object.entries(pairs).filter(([, v]) => v !== void 0 && v !== null && v !== false && v !== "").map(([k, v]) => ` ${k}="${esc(String(v))}"`).join("");
243
- const MARK_TAGS = {
244
- strong: "strong",
245
- em: "em",
246
- code: "code",
247
- strike: "s",
248
- mark: "mark",
249
- sup: "sup",
250
- sub: "sub",
251
- underline: "u"
252
- };
253
- /** 外側から内側の順 */
254
- const MARK_ORDER = [
255
- "strong",
256
- "em",
257
- "strike",
258
- "underline",
259
- "mark",
260
- "sup",
261
- "sub",
262
- "code"
263
- ];
264
269
  const CSS_PROPERTY = {
265
270
  color: "color",
266
271
  background: "background-color",
@@ -294,7 +299,7 @@ function bodyToHtml(body, options) {
294
299
  if (typeof i === "string") return esc(i).replaceAll("\n", "<br>");
295
300
  if ("component" in i) return options.inlineComponent?.(i.component, i.props ?? {}, i.text ?? "") ?? esc(i.text ?? "");
296
301
  let html = esc(i.text).replaceAll("\n", "<br>");
297
- for (const m of [...MARK_ORDER].reverse()) if (i.marks?.includes(m)) html = `<${MARK_TAGS[m]}>${html}</${MARK_TAGS[m]}>`;
302
+ for (const m of sortMarks(i.marks).reverse()) html = `<${MARK_HTML_TAGS[m]}>${html}</${MARK_HTML_TAGS[m]}>`;
298
303
  if (i.style) {
299
304
  const a = styleAttr(i.style);
300
305
  if (a) html = `<span${a}>${html}</span>`;
package/dist/delivery.js CHANGED
@@ -250,12 +250,22 @@ const EXTENSIONS = {
250
250
  "image/webp": "webp",
251
251
  "image/gif": "gif",
252
252
  "video/mp4": "mp4",
253
- "video/webm": "webm"
253
+ "video/webm": "webm",
254
+ "application/pdf": "pdf",
255
+ "application/zip": "zip",
256
+ "text/plain": "txt",
257
+ "text/csv": "csv",
258
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
259
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
260
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx"
254
261
  };
262
+ /** 形式の実体の種類 */
263
+ const mediaKindOf = (mimeType) => mimeType.startsWith("image/") ? "image" : mimeType.startsWith("video/") ? "video" : "file";
255
264
  /** 拡張子の形式(配信しない拡張子は null) */
256
265
  const mediaMimeOf = (ext) => Object.entries(EXTENSIONS).find(([, e]) => e === ext)?.[0] ?? null;
257
266
  const IMAGE_PATH = /^\/images\/([A-Za-z0-9_-]+)\/([A-Za-z0-9_-]+)-(\d+)x(\d+)\.([a-z0-9]+)$/;
258
267
  const VIDEO_PATH = /^\/videos\/([A-Za-z0-9_-]+)\/([A-Za-z0-9_-]+)\.([a-z0-9]+)$/;
268
+ const FILE_PATH = /^\/files\/([A-Za-z0-9_-]+)\/([A-Za-z0-9_-]+)\.([a-z0-9]+)$/;
259
269
  /** 配信 URL のパスを解析(形が違えば null) */
260
270
  function parseMediaPath(pathname) {
261
271
  const image = IMAGE_PATH.exec(pathname);
@@ -280,8 +290,39 @@ function parseMediaPath(pathname) {
280
290
  ext
281
291
  } : null;
282
292
  }
293
+ const file = FILE_PATH.exec(pathname);
294
+ if (file) {
295
+ const [, project, id, ext] = file;
296
+ const mime = mediaMimeOf(ext);
297
+ return mime && mediaKindOf(mime) === "file" ? {
298
+ kind: "file",
299
+ project,
300
+ id,
301
+ ext
302
+ } : null;
303
+ }
283
304
  return null;
284
305
  }
306
+ const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
307
+ /** 署名(HMAC-SHA256、対象はパスと期限、変換のクエリは含めない) */
308
+ async function signatureOf(secret, pathname, exp) {
309
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
310
+ name: "HMAC",
311
+ hash: "SHA-256"
312
+ }, false, ["sign"]);
313
+ return hex(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${pathname}:${exp}`)));
314
+ }
315
+ /**
316
+ * 非公開のメディアの署名つきの URL(exp と sig のクエリを付ける、ほかのクエリは残す)
317
+ * - secret: 媒体の非公開のメディアの署名の鍵、expiresInSec: 期限までの秒、now: 今の UNIX ミリ秒
318
+ */
319
+ async function signMediaUrl(url, secret, options = {}) {
320
+ const u = new URL(url);
321
+ const exp = Math.floor((options.now ?? Date.now()) / 1e3) + (options.expiresInSec ?? 3600);
322
+ u.searchParams.set("exp", String(exp));
323
+ u.searchParams.set("sig", await signatureOf(secret, u.pathname, exp));
324
+ return u.toString();
325
+ }
285
326
  /** 幅・高さの上限(画素) */
286
327
  const IMAGE_SIZE_MAX = 4096;
287
328
  const FITS = [
@@ -432,4 +473,4 @@ function createDeliveryClock(timeZone) {
432
473
  })
433
474
  };
434
475
  }
435
- export { DELIVERY_PATH, DeliveryError, cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, readChanges, subPath, withImageParams };
476
+ export { DELIVERY_PATH, DeliveryError, cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, readChanges, signMediaUrl, subPath, withImageParams };
package/dist/dev.js CHANGED
@@ -49,6 +49,11 @@ function relayDelivery({ origin, apiKey, previewSecret }) {
49
49
  });
50
50
  };
51
51
  }
52
+ const DECODED_BODY_HEADERS = /* @__PURE__ */ new Set([
53
+ "content-encoding",
54
+ "content-length",
55
+ "transfer-encoding"
56
+ ]);
52
57
  /** 手元の配信 API のサーバー(/content/v1/… だけ、失敗は502、戻り値は origin と停止の関数) */
53
58
  async function serveDelivery(delivery, options = {}) {
54
59
  const server = createServer(async (req, res) => {
@@ -67,7 +72,9 @@ async function serveDelivery(delivery, options = {}) {
67
72
  ...chunks.length ? { body: Buffer.concat(chunks).toString("utf8") } : {}
68
73
  });
69
74
  const out = {};
70
- response.headers.forEach((v, k) => out[k] = v);
75
+ response.headers.forEach((v, k) => {
76
+ if (!DECODED_BODY_HEADERS.has(k)) out[k] = v;
77
+ });
71
78
  res.writeHead(response.status, out);
72
79
  res.end(Buffer.from(await response.arrayBuffer()));
73
80
  } catch (e) {
package/dist/model.js CHANGED
@@ -344,11 +344,13 @@ const fieldUnion = z.discriminatedUnion("type", [
344
344
  type: z.literal("media"),
345
345
  accept: z.array(z.enum(["image", "video"])).optional(),
346
346
  multiple: z.boolean().optional(),
347
- frames: z.array(frame).optional()
347
+ frames: z.array(frame).optional(),
348
+ visibility: z.literal("private").optional()
348
349
  }),
349
350
  z.object({
350
351
  ...base,
351
352
  type: z.literal("body"),
353
+ visibility: z.literal("private").optional(),
352
354
  profile: z.object({
353
355
  headings: z.array(z.number()),
354
356
  marks: z.array(z.string()),
@@ -509,6 +511,9 @@ const modelSchema = z.object({
509
511
  /** コンテンツスキーマの JSON を検査して返却(形が合わなければ例外、理由は zod の issues) */
510
512
  const parseModel = (json) => modelSchema.parse(normalizeLegacyModel(json));
511
513
  // ../model/src/screens.ts
514
+ const TYPE_PREFIX = "type:";
515
+ /** メニュー項目の id(コンテンツタイプ) */
516
+ const typeItem = (type) => `${TYPE_PREFIX}${type}`;
512
517
  /** メニューにある全項目の id(グループの中も、上から順) */
513
518
  const menuItemsOf = (menu) => menu.flatMap((e) => typeof e === "string" ? [e] : e.items);
514
519
  /** 編集画面に配置した項目の id(セクションの順) */
@@ -542,296 +547,203 @@ const parseScreens = (json) => screensSchema.parse(json);
542
547
  // ../cms-contract/src/transfer.ts
543
548
  /** スキーマと設定の形式の名前 */
544
549
  const CONFIG_FORMAT = "cms-config";
545
- // src/defaults.ts
546
550
  /** 中立の既定のスキーマ(新しい媒体の作成で適用する型: post・author・tag・image・video、設定の移入の土台) */
547
- const DEFAULT_SCHEMA = { "types": [
551
+ const DEFAULT_SCHEMA = { types: [
548
552
  {
549
- "id": "post",
550
- "label": "投稿",
551
- "publishing": "review",
552
- "roles": {
553
- "title": "title",
554
- "image": "image",
555
- "body": "body"
553
+ id: "post",
554
+ label: "投稿",
555
+ publishing: "review",
556
+ roles: {
557
+ title: "title",
558
+ image: "image",
559
+ body: "body"
556
560
  },
557
- "fields": [
561
+ fields: [
558
562
  {
559
- "key": "title",
560
- "label": "タイトル",
561
- "type": "text",
562
- "required": "save"
563
+ key: "title",
564
+ label: "タイトル",
565
+ type: "text",
566
+ required: "save"
563
567
  },
564
568
  {
565
- "key": "writers",
566
- "label": "著者",
567
- "type": "reference",
568
- "to": "author",
569
- "multiple": true
569
+ key: "writers",
570
+ label: "著者",
571
+ type: "reference",
572
+ to: "author",
573
+ multiple: true
570
574
  },
571
575
  {
572
- "key": "image",
573
- "label": "メインの画像",
574
- "type": "media",
575
- "accept": ["image"]
576
+ key: "image",
577
+ label: "メインの画像",
578
+ type: "media",
579
+ accept: ["image"]
576
580
  },
577
581
  {
578
- "key": "body",
579
- "label": "本文",
580
- "type": "body",
581
- "profile": {
582
- "headings": [
582
+ key: "body",
583
+ label: "本文",
584
+ type: "body",
585
+ profile: {
586
+ headings: [
583
587
  2,
584
588
  3,
585
589
  4
586
590
  ],
587
- "marks": [
591
+ marks: [
588
592
  "strong",
589
593
  "em",
590
594
  "code",
591
595
  "strike"
592
596
  ],
593
- "list": {
594
- "ordered": true,
595
- "nested": true
597
+ list: {
598
+ ordered: true,
599
+ nested: true
596
600
  },
597
- "quote": {
598
- "multiple": true,
599
- "cite": true
601
+ quote: {
602
+ multiple: true,
603
+ cite: true
600
604
  },
601
- "code": { "caption": false },
602
- "table": {
603
- "head": "optional",
604
- "caption": "optional"
605
+ code: { caption: false },
606
+ table: {
607
+ head: "optional",
608
+ caption: "optional"
605
609
  },
606
- "divider": true,
607
- "components": ["gallery", "link-card"]
610
+ divider: true,
611
+ components: ["gallery", "link-card"]
608
612
  }
609
613
  },
610
614
  {
611
- "key": "revised_at",
612
- "label": "更新日時",
613
- "type": "datetime"
615
+ key: "revised_at",
616
+ label: "更新日時",
617
+ type: "datetime"
614
618
  },
615
619
  {
616
- "key": "tags",
617
- "label": "タグ",
618
- "type": "reference",
619
- "to": "tag",
620
- "multiple": true
620
+ key: "tags",
621
+ label: "タグ",
622
+ type: "reference",
623
+ to: "tag",
624
+ multiple: true
621
625
  },
622
626
  {
623
- "key": "excerpt",
624
- "label": "抜粋",
625
- "type": "textarea",
626
- "computed": {
627
- "kind": "excerpt",
628
- "from": "body"
627
+ key: "excerpt",
628
+ label: "抜粋",
629
+ type: "textarea",
630
+ computed: {
631
+ kind: "excerpt",
632
+ from: "body"
629
633
  }
630
634
  },
631
635
  {
632
- "key": "read_min",
633
- "label": "読了の分",
634
- "type": "number",
635
- "computed": {
636
- "kind": "read_min",
637
- "from": "body"
636
+ key: "read_min",
637
+ label: "読了の分",
638
+ type: "number",
639
+ computed: {
640
+ kind: "read_min",
641
+ from: "body"
638
642
  }
639
643
  },
640
644
  {
641
- "key": "links",
642
- "label": "リンク先",
643
- "type": "text",
644
- "multiple": true,
645
- "computed": {
646
- "kind": "links",
647
- "from": "body"
645
+ key: "links",
646
+ label: "リンク先",
647
+ type: "text",
648
+ multiple: true,
649
+ computed: {
650
+ kind: "links",
651
+ from: "body"
648
652
  }
649
653
  }
650
654
  ],
651
- "pages": [{
652
- "path": "/posts/{id}",
653
- "public": [{
654
- "key": "status",
655
- "in": ["published"]
655
+ pages: [{
656
+ path: "/posts/{id}",
657
+ public: [{
658
+ key: "status",
659
+ in: ["published"]
656
660
  }, {
657
- "key": "published_at",
658
- "notFuture": true
661
+ key: "published_at",
662
+ notFuture: true
659
663
  }]
660
664
  }]
661
665
  },
662
666
  {
663
- "id": "author",
664
- "label": "著者",
665
- "roles": {
666
- "title": "name",
667
- "image": "portrait"
667
+ id: "author",
668
+ label: "著者",
669
+ roles: {
670
+ title: "name",
671
+ image: "portrait"
668
672
  },
669
- "fields": [
673
+ fields: [
670
674
  {
671
- "key": "name",
672
- "label": "名前",
673
- "type": "text",
674
- "required": "save"
675
+ key: "name",
676
+ label: "名前",
677
+ type: "text",
678
+ required: "save"
675
679
  },
676
680
  {
677
- "key": "bio",
678
- "label": "紹介",
679
- "type": "textarea"
681
+ key: "bio",
682
+ label: "紹介",
683
+ type: "textarea"
680
684
  },
681
685
  {
682
- "key": "portrait",
683
- "label": "顔の画像",
684
- "type": "media",
685
- "accept": ["image"]
686
+ key: "portrait",
687
+ label: "顔の画像",
688
+ type: "media",
689
+ accept: ["image"]
686
690
  }
687
691
  ],
688
- "pages": [{ "path": "/authors/{id}" }]
692
+ pages: [{ path: "/authors/{id}" }]
689
693
  },
690
694
  {
691
- "id": "tag",
692
- "label": "タグ",
693
- "roles": { "title": "label" },
694
- "fields": [{
695
- "key": "label",
696
- "label": "名前",
697
- "type": "text",
698
- "required": "save"
695
+ id: "tag",
696
+ label: "タグ",
697
+ roles: { title: "label" },
698
+ fields: [{
699
+ key: "label",
700
+ label: "名前",
701
+ type: "text",
702
+ required: "save"
699
703
  }]
700
704
  },
701
- {
702
- "id": "image",
703
- "label": "画像",
704
- "description": "見せたい点(hotspot)と切り抜き(crop)を画像ごとに1つ保持、切り抜く場所すべてで共通",
705
- "fields": [
706
- {
707
- "key": "mime_type",
708
- "label": "形式",
709
- "type": "text",
710
- "required": "save",
711
- "description": "MIME タイプ"
712
- },
713
- {
714
- "key": "size",
715
- "label": "サイズ",
716
- "type": "number",
717
- "required": "save",
718
- "description": "バイト数"
719
- },
720
- {
721
- "key": "width",
722
- "label": "幅",
723
- "type": "number",
724
- "required": "save",
725
- "description": "px"
726
- },
727
- {
728
- "key": "height",
729
- "label": "高さ",
730
- "type": "number",
731
- "required": "save",
732
- "description": "px"
733
- },
734
- {
735
- "key": "alt",
736
- "label": "代替テキスト",
737
- "type": "text",
738
- "description": "使用する側の既定の値、使用する場所ごとに上書き可"
739
- }
740
- ]
741
- },
742
- {
743
- "id": "video",
744
- "label": "動画",
745
- "fields": [
746
- {
747
- "key": "mime_type",
748
- "label": "形式",
749
- "type": "text",
750
- "required": "save",
751
- "description": "MIME タイプ"
752
- },
753
- {
754
- "key": "size",
755
- "label": "サイズ",
756
- "type": "number",
757
- "required": "save",
758
- "description": "バイト数"
759
- },
760
- {
761
- "key": "width",
762
- "label": "幅",
763
- "type": "number",
764
- "required": "save",
765
- "description": "px"
766
- },
767
- {
768
- "key": "height",
769
- "label": "高さ",
770
- "type": "number",
771
- "required": "save",
772
- "description": "px"
773
- },
774
- {
775
- "key": "alt",
776
- "label": "代替テキスト",
777
- "type": "text",
778
- "description": "使用する側の既定の値、使用する場所ごとに上書き可"
779
- },
780
- {
781
- "key": "duration",
782
- "label": "長さ",
783
- "type": "number",
784
- "description": "秒"
785
- },
786
- {
787
- "key": "poster",
788
- "label": "ポスター画像",
789
- "type": "media",
790
- "accept": ["image"],
791
- "description": "読み込み前・動きを抑える設定・OG で代わりに表示"
792
- }
793
- ]
794
- }
705
+ IMAGE_TYPE,
706
+ VIDEO_TYPE
795
707
  ] };
796
708
  /** 既定の画面設定(新しい媒体の作成で適用する中立の値、移入する screens の土台) */
797
709
  const DEFAULT_SCREENS = {
798
- "menu": [
710
+ menu: [
799
711
  "dashboard",
800
712
  "calendar",
801
713
  {
802
- "id": "content",
803
- "label": "コンテンツ",
804
- "items": ["type:post", "media"]
714
+ id: "content",
715
+ label: "コンテンツ",
716
+ items: [typeItem("post"), "media"]
805
717
  },
806
718
  {
807
- "id": "masters",
808
- "label": "マスタ",
809
- "items": ["type:tag", "type:author"]
719
+ id: "masters",
720
+ label: "マスタ",
721
+ items: [typeItem("tag"), typeItem("author")]
810
722
  }
811
723
  ],
812
- "editors": { "post": { "sections": [
724
+ editors: { post: { sections: [
813
725
  {
814
- "id": "classify",
815
- "label": "分類",
816
- "items": ["writers", "tags"]
726
+ id: "classify",
727
+ label: "分類",
728
+ items: ["writers", "tags"]
817
729
  },
818
730
  {
819
- "id": "publish",
820
- "label": "公開",
821
- "items": ["published_at", "revised_at"]
731
+ id: "publish",
732
+ label: "公開",
733
+ items: ["published_at", "revised_at"]
822
734
  },
823
735
  {
824
- "id": "excerpt",
825
- "label": "説明",
826
- "items": ["excerpt"]
736
+ id: "excerpt",
737
+ label: "説明",
738
+ items: ["excerpt"]
827
739
  },
828
740
  {
829
- "id": "image",
830
- "label": "メインの画像",
831
- "items": ["image"]
741
+ id: "image",
742
+ label: "メインの画像",
743
+ items: ["image"]
832
744
  }
833
745
  ] } },
834
- "columns": { "post": [
746
+ columns: { post: [
835
747
  "status",
836
748
  "writers",
837
749
  "assignee",
@@ -0,0 +1,20 @@
1
+ import type { Mark, TextStyle } from './types.js';
2
+ /** 装飾の順序(外側から内側、Markdown と HTML の入れ子の順) */
3
+ export declare const MARK_ORDER: readonly ["strong", "em", "strike", "underline", "mark", "sup", "sub", "code"];
4
+ /** 装飾を MARK_ORDER の順に並べる(重複は1つ) */
5
+ export declare const sortMarks: (marks: readonly Mark[] | undefined) => Mark[];
6
+ /** 装飾の描画の HTML の要素 */
7
+ export declare const MARK_HTML_TAGS: {
8
+ readonly strong: "strong";
9
+ readonly em: "em";
10
+ readonly code: "code";
11
+ readonly strike: "s";
12
+ readonly mark: "mark";
13
+ readonly sup: "sup";
14
+ readonly sub: "sub";
15
+ readonly underline: "u";
16
+ };
17
+ /** HTML の要素の装飾(描画の要素と、リッチエディタが使うほかの要素) */
18
+ export declare const HTML_TAG_MARKS: Readonly<Record<string, Mark>>;
19
+ /** 内側の見た目に外側の見た目を重ねる(内側を優先、名前のついた装飾は合わせる) */
20
+ export declare const mergeStyle: (outer: TextStyle | undefined, inner: TextStyle | undefined) => TextStyle | undefined;
@@ -1,6 +1,4 @@
1
- import { type Body, type Mark, type Rich } from './types.js';
2
- /** 装飾の順序(Markdown では外側から内側) */
3
- export declare const MARK_ORDER: readonly Mark[];
1
+ import { type Body, type Rich } from './types.js';
4
2
  /** インラインの断片の列を正規化 */
5
3
  export declare function normalizeRich(rich: Rich): Rich;
6
4
  /** 本文を正規化 */
@@ -8,3 +8,5 @@ export declare const ASSET_BULK_MAX = 500;
8
8
  export declare const ASSET_IMAGE_TYPES: readonly ["image/jpeg", "image/png", "image/webp", "image/gif"];
9
9
  /** 画像1枚の大きさの上限(バイト、アップロード・取り込みで共通) */
10
10
  export declare const ASSET_SIZE_MAX: number;
11
+ /** ファイルの実体の大きさの上限(バイト、本文の中のファイルの取り込み) */
12
+ export declare const ASSET_FILE_SIZE_MAX: number;
@@ -10,6 +10,7 @@ export declare const ERROR_STATUS: {
10
10
  readonly file_required: 400;
11
11
  readonly file_too_large: 400;
12
12
  readonly unsupported_image: 400;
13
+ readonly unsupported_file: 400;
13
14
  readonly invalid_dimensions: 400;
14
15
  readonly https_required: 400;
15
16
  readonly invalid_style: 400;
@@ -74,7 +75,7 @@ export type ErrorParams = {
74
75
  /** 状態の不一致の理由: invalid_state */
75
76
  reason?: 'not_draft' | 'no_draft' | 'not_in_review' | 'request_handled' | 'deleted' | 'not_deleted' | 'schema_confirm' | 'no_publishing' | 'no_review' | 'not_published';
76
77
  /** 取得の対象: upstream_failed */
77
- resource?: 'page' | 'image' | 'webhook' | 'microcms';
78
+ resource?: 'page' | 'image' | 'webhook' | 'microcms' | 'file';
78
79
  /**
79
80
  * 名前
80
81
  * - no_linked_entry: 結び付くコンテンツタイプの表示名、account_not_found: メールアドレス
@@ -107,6 +107,7 @@ export type MicroCmsApiInput = Readonly<{
107
107
  format: 'list' | 'object';
108
108
  schema: unknown;
109
109
  relations: Readonly<Record<string, string>>;
110
+ privateMedia?: boolean;
110
111
  }>;
111
112
  /** microCMS の型の変換の注意(path は型の id と項目) */
112
113
  export type MicroCmsNote = Readonly<{
@@ -1,5 +1,5 @@
1
1
  export { API_KEY_ALL_TYPES, API_KEY_KINDS, API_KEY_OPERATIONS, API_KEY_RATE, API_KEY_WRITE_OPERATIONS, PUBLIC_FAILURE_RATE, apiKeyAllows, type ApiKey, type ApiKeyKind, type ApiKeyOperation, type ApiKeyPermissions, DEFAULT_API_KEY_PERMISSIONS } from './apiKeys.js';
2
- export { ASSET_BULK_MAX, ASSET_IMAGE_TYPES, ASSET_SIZE_MAX } from './assets.js';
2
+ export { ASSET_BULK_MAX, ASSET_FILE_SIZE_MAX, ASSET_IMAGE_TYPES, ASSET_SIZE_MAX } from './assets.js';
3
3
  export { errorLabel, errorTitle, mergeMessages, TARGET_LABELS, targetOf, type TargetMessages } from './errorLabels.js';
4
4
  export { ERROR_STATUS, JSON_BODY_MAX, type ErrorBody, type ErrorCode, type ErrorParams, type ErrorTarget } from './errors.js';
5
5
  export { CHANGES_LABELS, STATUS } from './labels.js';
@@ -25,7 +25,7 @@ export type LocalImage = Readonly<{
25
25
  height: number;
26
26
  format: string;
27
27
  }>;
28
- /** 実体の共通の項目(ID は UUID で R2 のキーと同じ、sha256 は内容のハッシュで同じ画像の重複を防止、ファイル名は保存しない) */
28
+ /** 実体の共通の項目(ID は UUID で R2 のキーと同じ、sha256 は内容のハッシュで同じ画像の重複を防止、visibility は非公開の実体だけ private) */
29
29
  type AssetBase = Readonly<{
30
30
  id: string;
31
31
  mimeType: string;
@@ -37,6 +37,7 @@ type AssetBase = Readonly<{
37
37
  hotspot?: Hotspot;
38
38
  crop?: Crop;
39
39
  createdAt: string;
40
+ visibility?: 'private';
40
41
  }>;
41
42
  /** 画像の実体(Sanity の sanity.imageAsset、代替テキストと hotspot は画像ごとに1つ) */
42
43
  export type ImageAsset = AssetBase & Readonly<{
@@ -48,12 +49,33 @@ export type VideoAsset = AssetBase & Readonly<{
48
49
  duration?: number;
49
50
  poster?: string;
50
51
  }>;
52
+ /** ファイルの実体(本文の中のファイル、name は元のファイル名、寸法は0) */
53
+ export type FileAsset = AssetBase & Readonly<{
54
+ kind: 'file';
55
+ name?: string;
56
+ }>;
51
57
  /** 実体(CMS のメディアにある1件) */
52
- export type Asset = ImageAsset | VideoAsset;
58
+ export type Asset = ImageAsset | VideoAsset | FileAsset;
53
59
  /** 使用する側が保存する実体の参照(Sanity の image の asset、表示のときに実体の寸法・代替テキスト・hotspot を合わせて Media に解決) */
54
60
  export type AssetRef = Readonly<{
55
61
  asset: string;
56
62
  }>;
63
+ /** 配信 API が解決した実体(url は配信 URL で配信のオリジンがある CMS だけ、hotspot・crop は元の画像に対する割合) */
64
+ export type DeliveredAsset = Readonly<{
65
+ id: string;
66
+ kind: 'image' | 'video' | 'file';
67
+ url?: string;
68
+ width: number;
69
+ height: number;
70
+ alt: string;
71
+ hotspot?: Hotspot;
72
+ crop?: Crop;
73
+ poster?: DeliveredAsset | null; /** 非公開の実体(url は署名のない配信 URL、signMediaUrl で署名して渡す) */
74
+ visibility?: 'private'; /** ファイルの元のファイル名・形式・大きさ(バイト) */
75
+ name?: string;
76
+ mimeType?: string;
77
+ size?: number;
78
+ }>;
57
79
  /**
58
80
  * 表示する素材の共通の項目
59
81
  * - src: URL(CMS の実体)か、ビルドで読み込んだ画像
@@ -1,14 +1,14 @@
1
1
  export { fieldZod, jsonSchemaOf, normalizeValues, zodOf, type ValueOptions } from './input.js';
2
- export { assetRefSchema, cropSchema, hotspotSchema, mediaSchema, type Asset, type AssetRef, type Crop, type Hotspot, type LocalImage, type Media } from './asset.js';
2
+ export { assetRefSchema, cropSchema, hotspotSchema, mediaSchema, type Asset, type AssetRef, type Crop, type DeliveredAsset, type Hotspot, type LocalImage, type Media } from './asset.js';
3
3
  export { coverPosition, coverStart, cropRect, croppedHotspot, objectPosition, stillOf, type CropRect } from './image.js';
4
4
  export { IMAGE_TYPE, VIDEO_TYPE } from './media.js';
5
- export { IMAGE_SIZE_MAX, imageParamsOf, mediaExtOf, mediaMimeOf, mediaPathOf, parseMediaPath, withImageParams, type ImageParams, type MediaPath } from './mediaUrl.js';
5
+ export { IMAGE_SIZE_MAX, imageParamsOf, MEDIA_SIGNING_KEY_BLOB, mediaBlobKeyOf, mediaExtOf, mediaKindOf, mediaMimeOf, mediaPathOf, parseMediaPath, signMediaUrl, verifyMediaUrl, withImageParams, type ImageParams, type AssetKind, type MediaPath } from './mediaUrl.js';
6
6
  export { diffSchema, renameType, type ChangeKind, type ChangeLevel, type DiffOptions, type SchemaChange } from './changes.js';
7
7
  export { dateRoleFieldsOf, primaryTypeOf, requiredPrimaryTypeOf, SOURCES_FIELD, sourceIdsOf, sourcesFieldOf, sourceTypeOf, type SourcesField } from './capabilities.js';
8
8
  export { appliesTo, checkFields, fieldsFor } from './check.js';
9
9
  export { deliveredFields, emptyValues, pickValues, referencedIds, referencesOf, valueFieldsOf } from './values.js';
10
10
  export { entryRefsOf, type EntryRef } from './refs.js';
11
- export { acceptsRole, bodyFieldsOf, computedFieldsOf, FIELD_ROLES, idInputOf, initialValueOf, isEditorial, isInputField, isListType, isMediaType, isSingleton, isStoredType, MEDIA_TYPE_IDS, optionLabelOf, optionsOf, publishingOf, requestReferencesOf, resolvedRolesOf, roleFieldOf, toneOf, typeOf, type ComputedKind, type FieldRole, type FieldRoles, type FieldComputed, type ContentFormat, type ContentModel, type ContentType, type FieldCondition, type FieldDef, type FieldDerive, type FieldFixedBy, type CountDef, type FieldProblem, type MediaKind, type OptionTone, type Publishing, type ReferenceRules, type SelectOption, usesReview } from './model.js';
11
+ export { acceptsRole, bodyFieldsOf, computedFieldsOf, FIELD_ROLES, idInputOf, initialValueOf, isPublishable, isInputField, isListType, isMediaType, isSingleton, isStoredType, MEDIA_TYPE_IDS, optionLabelOf, optionsOf, publishingOf, requestReferencesOf, resolvedRolesOf, roleFieldOf, toneOf, typeOf, type ComputedKind, type FieldRole, type FieldRoles, type FieldComputed, type ContentFormat, type ContentModel, type ContentType, type FieldCondition, type FieldDef, type FieldDerive, type FieldFixedBy, type CountDef, type FieldProblem, type MediaKind, type OptionTone, type Publishing, type ReferenceRules, type SelectOption, usesReview } from './model.js';
12
12
  export { hasPublicPage, imageFrameOf, pagePathOf, publicPathOf, withPagesFrom, type ImageFrame, type ImageFrameDef, type PageDef, type PageEntry, type PageRule } from './page.js';
13
13
  export { schemaRulesOf, type SchemaRule } from './rules.js';
14
14
  export { parseModel } from './schema.js';
@@ -1,11 +1,15 @@
1
1
  import type { CropRect } from './image.js';
2
+ /** 実体の種類 */
3
+ export type AssetKind = 'image' | 'video' | 'file';
4
+ /** 形式の実体の種類 */
5
+ export declare const mediaKindOf: (mimeType: string) => AssetKind;
2
6
  /** 形式の拡張子(配信しない形式は null) */
3
7
  export declare const mediaExtOf: (mimeType: string) => string | null;
4
8
  /** 拡張子の形式(配信しない拡張子は null) */
5
9
  export declare const mediaMimeOf: (ext: string) => string | null;
6
10
  /** 配信 URL のパスの要素(寸法は画像だけ) */
7
11
  export type MediaPath = Readonly<{
8
- kind: 'image' | 'video';
12
+ kind: AssetKind;
9
13
  project: string;
10
14
  id: string;
11
15
  ext: string;
@@ -14,7 +18,7 @@ export type MediaPath = Readonly<{
14
18
  }>;
15
19
  /** 配信 URL のパス(媒体・ID の形が不正、配信しない形式なら null) */
16
20
  export declare function mediaPathOf({ kind, project, id, mimeType, width, height }: Readonly<{
17
- kind: 'image' | 'video';
21
+ kind: AssetKind;
18
22
  project: string;
19
23
  id: string;
20
24
  mimeType: string;
@@ -23,6 +27,20 @@ export declare function mediaPathOf({ kind, project, id, mimeType, width, height
23
27
  }>): string | null;
24
28
  /** 配信 URL のパスを解析(形が違えば null) */
25
29
  export declare function parseMediaPath(pathname: string): MediaPath | null;
30
+ /** 保存先のキー(媒体の接頭辞の後、非公開は private/ の下) */
31
+ export declare const mediaBlobKeyOf: (id: string, visibility: "public" | "private") => string;
32
+ /** 非公開のメディアの署名の鍵の保存先のキー(媒体の接頭辞の後、配信 URL の ID の形にない「.」で始め、パスから届かない) */
33
+ export declare const MEDIA_SIGNING_KEY_BLOB = ".media-signing";
34
+ /**
35
+ * 非公開のメディアの署名つきの URL(exp と sig のクエリを付ける、ほかのクエリは残す)
36
+ * - secret: 媒体の非公開のメディアの署名の鍵、expiresInSec: 期限までの秒、now: 今の UNIX ミリ秒
37
+ */
38
+ export declare function signMediaUrl(url: string, secret: string, options?: Readonly<{
39
+ expiresInSec?: number;
40
+ now?: number;
41
+ }>): Promise<string>;
42
+ /** 署名つきの URL の検査(期限の内で、どれかの鍵の署名と一致すれば期限の UNIX 秒、ほかは null) */
43
+ export declare function verifyMediaUrl(url: URL, secrets: readonly string[], now?: number): Promise<number | null>;
26
44
  /** 画像の変換のクエリ(w・h は表示の画素、rect は元の画像の画素での切り抜き、auto=format はブラウザに合わせた形式) */
27
45
  export type ImageParams = Readonly<{
28
46
  w?: number;
@@ -112,10 +112,12 @@ export type FieldDef = FieldBase & Readonly<{
112
112
  type: 'media'; /** 受け付けるメディアの種類(なしはすべて) */
113
113
  accept?: readonly MediaKind[];
114
114
  multiple?: boolean; /** 画像の枠(先頭から条件に合うもの、page.ts) */
115
- frames?: readonly ImageFrameDef[];
115
+ frames?: readonly ImageFrameDef[]; /** 非公開のメディアだけ(アップロードと取り込みは非公開、公開の実体は保存で拒否) */
116
+ visibility?: 'private';
116
117
  } & Counts) | {
117
118
  type: 'body';
118
- profile: BodyProfile;
119
+ profile: BodyProfile; /** 本文の画像とファイルは非公開のメディアだけ */
120
+ visibility?: 'private';
119
121
  } | ({
120
122
  type: 'group'; /** まとまりの中の項目(Sanity の object、microCMS のカスタムフィールド) */
121
123
  fields: readonly FieldDef[];
@@ -181,7 +183,7 @@ export declare const isListType: (type: ContentType) => boolean;
181
183
  * - 機能: 版・楽観ロック・編集中のリース・コメント・公開の前の点検
182
184
  * - 提出と差し戻し・担当者: レビューありの型だけ(usesReview)
183
185
  */
184
- export declare const isEditorial: (type: ContentType) => boolean;
186
+ export declare const isPublishable: (type: ContentType) => boolean;
185
187
  /**
186
188
  * レビューのワークフローを持つ型か(レビューありの一覧の型)
187
189
  * - 機能: 提出と差し戻し・担当者(レビューを担当するアカウント)
@@ -1,4 +1,7 @@
1
- import type { ContentModel, ScreenSettings } from '../../model/src/index.js';
1
+ import type { BodyProfile } from '../../body/src/index.js';
2
+ import { type ContentModel, type ScreenSettings } from '../../model/src/index.js';
3
+ /** 既定の本文の設定(本文の項目 profile と同じ値) */
4
+ export declare const DEFAULT_BODY_PROFILE: BodyProfile;
2
5
  /** 中立の既定のスキーマ(新しい媒体の作成で適用する型: post・author・tag・image・video、設定の移入の土台) */
3
6
  export declare const DEFAULT_SCHEMA: ContentModel;
4
7
  /** 既定の画面設定(新しい媒体の作成で適用する中立の値、移入する screens の土台) */
@@ -2,8 +2,8 @@ export { cachedDelivery, memoryDeliveryCache, type DeliveryCache } from './cache
2
2
  export { readChanges, type Change, type ChangesPage } from './changes.js';
3
3
  export { createDeliveryClient, DELIVERY_PATH, DeliveryError, subPath, type BundleResult, type Delivery, type DeliveryClient, type Prefetched } from './client.js';
4
4
  export { deliveredImageUrl, mediaOf, type ImageUrl, type MediaOf } from './media.js';
5
- export { imageParamsOf, parseMediaPath, withImageParams, type ImageParams, type MediaPath } from '../../../model/src/index.js';
5
+ export { imageParamsOf, parseMediaPath, signMediaUrl, withImageParams, type DeliveredAsset, type ImageParams, type MediaPath } from '../../../model/src/index.js';
6
6
  export type { FieldPath, Selected } from './fields.js';
7
7
  export { listParams, type ListFilter, type ListQuery, type Page } from './query.js';
8
8
  export { createDeliveryClock, fieldOf, limitOf, optionLabels, pagePath, type DeliveryClock } from './schema.js';
9
- export type { DeliveredAsset, FieldValue, Item, ItemMeta, ItemOf } from './values.js';
9
+ export type { FieldValue, Item, ItemMeta, ItemOf } from './values.js';
@@ -1,19 +1,7 @@
1
1
  import type { Body } from '../../../body/src/index.js';
2
- import type { ContentType, Crop, DateString, FieldDef, Hotspot } from '../../../model/src/index.js';
2
+ import type { ContentType, DateString, DeliveredAsset, FieldDef } from '../../../model/src/index.js';
3
3
  /** 配信 API の1件の値(id・項目の key・メタ情報 _meta) */
4
4
  export type Item = Readonly<Record<string, unknown>>;
5
- /** 配信 API が解決した実体(url は配信 URL で配信のオリジンがある CMS だけ、hotspot・crop は元の画像に対する割合) */
6
- export type DeliveredAsset = Readonly<{
7
- id: string;
8
- kind: 'image' | 'video';
9
- url?: string;
10
- width: number;
11
- height: number;
12
- alt: string;
13
- hotspot?: Hotspot;
14
- crop?: Crop;
15
- poster?: DeliveredAsset | null;
16
- }>;
17
5
  /** 項目の1つ分の値(複数の値を持つ項目は FieldValue が配列に、query.ts の絞り込みの値と共通) */
18
6
  export type SingleValue<F extends FieldDef> = F extends {
19
7
  type: 'text' | 'textarea';
@@ -1,2 +1,2 @@
1
- export { cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, DELIVERY_PATH, DeliveryError, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, readChanges, subPath, withImageParams } from './delivery/index.js';
1
+ export { cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, DELIVERY_PATH, DeliveryError, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, readChanges, signMediaUrl, subPath, withImageParams } from './delivery/index.js';
2
2
  export type { BundleResult, Change, ChangesPage, DeliveredAsset, Delivery, DeliveryCache, DeliveryClient, DeliveryClock, FieldValue, ImageParams, ImageUrl, Item, ItemMeta, ItemOf, FieldPath, ListFilter, ListQuery, MediaOf, MediaPath, Page, Prefetched, Selected } from './delivery/index.js';
@@ -2,4 +2,4 @@ export { coverPosition, coverStart, createClock, cropRect, croppedHotspot, IMAGE
2
2
  export type { Asset, AssetRef, Clock, ContentModel, ContentType, Crop, CropRect, DateString, FieldDef, Hotspot, LocalImage, Media, PageDef, PageEntry, ScreenSettings } from '../../model/src/index.js';
3
3
  export { CONFIG_FORMAT } from '../../cms-contract/src/index.js';
4
4
  export type { ConfigBundle } from '../../cms-contract/src/index.js';
5
- export { DEFAULT_SCHEMA, DEFAULT_SCREENS } from './defaults.js';
5
+ export { DEFAULT_SCHEMA, DEFAULT_SCREENS } from '../../platform/src/defaultModel.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryusenkei/maezae",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "description": "maezae の導入先のサイト向けの SDK(配信 API の取得・本文の型・プレビューの署名・手元の開発の配信 API)",
5
5
  "license": "UNLICENSED",
6
6
  "author": "RYUSENKEI",